switch to compose-bom-alpha

This commit is contained in:
2025-06-20 21:25:17 +03:00
parent a916dc649c
commit 5aa1f21183
5 changed files with 208 additions and 454 deletions
@@ -50,7 +50,6 @@ import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.theme.LocalUser import dev.meloda.fast.ui.theme.LocalUser
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.KoinContext
import org.koin.compose.koinInject import org.koin.compose.koinInject
import dev.meloda.fast.ui.R as UiR import dev.meloda.fast.ui.R as UiR
@@ -89,169 +88,167 @@ class MainActivity : AppCompatActivity() {
requestNotificationPermissions() requestNotificationPermissions()
setContent { setContent {
KoinContext { val context = LocalContext.current
val context = LocalContext.current
val userSettings: UserSettings = koinInject() val userSettings: UserSettings = koinInject()
val longPollController: LongPollController = koinInject() val longPollController: LongPollController = koinInject()
val longPollCurrentState by longPollController.currentState.collectAsStateWithLifecycle() val longPollCurrentState by longPollController.currentState.collectAsStateWithLifecycle()
val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle() val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle()
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>() val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle() val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle()
LifecycleResumeEffect(true) { LifecycleResumeEffect(true) {
viewModel.onAppResumed(intent) viewModel.onAppResumed(intent)
onPauseOrDispose {} onPauseOrDispose {}
} }
val permissionState = val permissionState =
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS) rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
val isNeedToCheckPermission by viewModel.isNeedToCheckNotificationsPermission.collectAsStateWithLifecycle() val isNeedToCheckPermission by viewModel.isNeedToCheckNotificationsPermission.collectAsStateWithLifecycle()
val isNeedToRequestPermission by viewModel.isNeedToRequestNotifications.collectAsStateWithLifecycle() val isNeedToRequestPermission by viewModel.isNeedToRequestNotifications.collectAsStateWithLifecycle()
LaunchedEffect(isNeedToCheckPermission) { LaunchedEffect(isNeedToCheckPermission) {
if (isNeedToCheckPermission) { if (isNeedToCheckPermission) {
viewModel.onPermissionCheckStatus(permissionState.status) viewModel.onPermissionCheckStatus(permissionState.status)
if (permissionState.status.isGranted) { if (permissionState.status.isGranted) {
if (longPollCurrentState == LongPollState.InApp) { if (longPollCurrentState == LongPollState.InApp) {
toggleLongPollService(false)
}
toggleLongPollService(
enable = true,
inBackground = true
)
}
}
}
LaunchedEffect(isNeedToRequestPermission) {
if (isNeedToRequestPermission) {
viewModel.onPermissionsRequested()
permissionState.launchPermissionRequest()
}
}
LifecycleResumeEffect(longPollStateToApply) {
Log.d("LongPollMainActivity", "longPollStateToApply: $longPollStateToApply")
if (longPollStateToApply != LongPollState.Background) {
if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched()
&& longPollCurrentState != longPollStateToApply
) {
toggleLongPollService(false) toggleLongPollService(false)
Log.d("LongPoll", "recreate()")
} }
toggleLongPollService( toggleLongPollService(
enable = longPollStateToApply.isLaunched(), enable = true,
inBackground = longPollStateToApply == LongPollState.Background inBackground = true
) )
} }
onPauseOrDispose {}
} }
}
val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle() LaunchedEffect(isNeedToRequestPermission) {
LifecycleResumeEffect(sendOnline) { if (isNeedToRequestPermission) {
toggleOnlineService(sendOnline) viewModel.onPermissionsRequested()
permissionState.launchPermissionRequest()
}
}
onPauseOrDispose { LifecycleResumeEffect(longPollStateToApply) {
toggleOnlineService(false) Log.d("LongPollMainActivity", "longPollStateToApply: $longPollStateToApply")
if (longPollStateToApply != LongPollState.Background) {
if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched()
&& longPollCurrentState != longPollStateToApply
) {
toggleLongPollService(false)
Log.d("LongPoll", "recreate()")
} }
}
val deviceWidthDp = remember(true) { toggleLongPollService(
context.resources.displayMetrics.widthPixels.pxToDp() enable = longPollStateToApply.isLaunched(),
} inBackground = longPollStateToApply == LongPollState.Background
val deviceHeightDp = remember(true) {
context.resources.displayMetrics.heightPixels.pxToDp()
}
val deviceWidthSize by remember(deviceWidthDp) {
derivedStateOf {
when {
deviceWidthDp <= 360 -> DeviceSize.Small
deviceWidthDp <= 600 -> DeviceSize.Compact
deviceWidthDp <= 840 -> DeviceSize.Medium
else -> DeviceSize.Expanded
}
}
}
val deviceHeightSize by remember(deviceHeightDp) {
derivedStateOf {
when {
deviceHeightDp <= 480 -> DeviceSize.Small
deviceHeightDp <= 700 -> DeviceSize.Compact
deviceHeightDp <= 900 -> DeviceSize.Medium
else -> DeviceSize.Expanded
}
}
}
val sizeConfig by remember(deviceWidthSize, deviceHeightSize) {
mutableStateOf(
SizeConfig(
widthSize = deviceWidthSize,
heightSize = deviceHeightSize
)
) )
} }
val darkMode by userSettings.darkMode.collectAsStateWithLifecycle() onPauseOrDispose {}
val dynamicColors by userSettings.enableDynamicColors.collectAsStateWithLifecycle() }
val amoledDark by userSettings.enableAmoledDark.collectAsStateWithLifecycle()
val enableBlur by userSettings.useBlur.collectAsStateWithLifecycle()
val enableMultiline by userSettings.enableMultiline.collectAsStateWithLifecycle()
val useSystemFont by userSettings.useSystemFont.collectAsStateWithLifecycle()
val enableAnimations by userSettings.enableAnimations.collectAsStateWithLifecycle()
val setDarkMode = isNeedToEnableDarkMode(darkMode = darkMode) val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle()
LifecycleResumeEffect(sendOnline) {
toggleOnlineService(sendOnline)
val themeConfig by remember( onPauseOrDispose {
darkMode, toggleOnlineService(false)
dynamicColors, }
amoledDark, }
enableBlur,
enableMultiline, val deviceWidthDp = remember(true) {
setDarkMode, context.resources.displayMetrics.widthPixels.pxToDp()
useSystemFont }
) { val deviceHeightDp = remember(true) {
derivedStateOf { context.resources.displayMetrics.heightPixels.pxToDp()
ThemeConfig( }
darkMode = setDarkMode,
dynamicColors = dynamicColors, val deviceWidthSize by remember(deviceWidthDp) {
selectedColorScheme = 0, derivedStateOf {
amoledDark = amoledDark, when {
enableBlur = enableBlur, deviceWidthDp <= 360 -> DeviceSize.Small
enableMultiline = enableMultiline, deviceWidthDp <= 600 -> DeviceSize.Compact
useSystemFont = useSystemFont, deviceWidthDp <= 840 -> DeviceSize.Medium
enableAnimations = enableAnimations else -> DeviceSize.Expanded
)
} }
} }
}
CompositionLocalProvider( val deviceHeightSize by remember(deviceHeightDp) {
LocalThemeConfig provides themeConfig, derivedStateOf {
LocalSizeConfig provides sizeConfig, when {
LocalUser provides currentUser deviceHeightDp <= 480 -> DeviceSize.Small
) { deviceHeightDp <= 700 -> DeviceSize.Compact
AppTheme( deviceHeightDp <= 900 -> DeviceSize.Medium
useDarkTheme = themeConfig.darkMode, else -> DeviceSize.Expanded
useDynamicColors = themeConfig.dynamicColors,
selectedColorScheme = themeConfig.selectedColorScheme,
useAmoledBackground = themeConfig.amoledDark,
useSystemFont = themeConfig.useSystemFont
) {
RootScreen(viewModel = viewModel)
} }
} }
} }
val sizeConfig by remember(deviceWidthSize, deviceHeightSize) {
mutableStateOf(
SizeConfig(
widthSize = deviceWidthSize,
heightSize = deviceHeightSize
)
)
}
val darkMode by userSettings.darkMode.collectAsStateWithLifecycle()
val dynamicColors by userSettings.enableDynamicColors.collectAsStateWithLifecycle()
val amoledDark by userSettings.enableAmoledDark.collectAsStateWithLifecycle()
val enableBlur by userSettings.useBlur.collectAsStateWithLifecycle()
val enableMultiline by userSettings.enableMultiline.collectAsStateWithLifecycle()
val useSystemFont by userSettings.useSystemFont.collectAsStateWithLifecycle()
val enableAnimations by userSettings.enableAnimations.collectAsStateWithLifecycle()
val setDarkMode = isNeedToEnableDarkMode(darkMode = darkMode)
val themeConfig by remember(
darkMode,
dynamicColors,
amoledDark,
enableBlur,
enableMultiline,
setDarkMode,
useSystemFont
) {
derivedStateOf {
ThemeConfig(
darkMode = setDarkMode,
dynamicColors = dynamicColors,
selectedColorScheme = 0,
amoledDark = amoledDark,
enableBlur = enableBlur,
enableMultiline = enableMultiline,
useSystemFont = useSystemFont,
enableAnimations = enableAnimations
)
}
}
CompositionLocalProvider(
LocalThemeConfig provides themeConfig,
LocalSizeConfig provides sizeConfig,
LocalUser provides currentUser
) {
AppTheme(
useDarkTheme = themeConfig.darkMode,
useDynamicColors = themeConfig.dynamicColors,
selectedColorScheme = themeConfig.selectedColorScheme,
useAmoledBackground = themeConfig.amoledDark,
useSystemFont = themeConfig.useSystemFont
) {
RootScreen(viewModel = viewModel)
}
}
} }
} }
@@ -1,19 +1,16 @@
package dev.meloda.fast.ui.components package dev.meloda.fast.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Indication
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButtonColors import androidx.compose.material3.IconButtonColors
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalUseFallbackRippleImplementation
import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.material3.ripple import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -23,10 +20,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun IconButton( fun IconButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@@ -39,21 +35,18 @@ fun IconButton(
) { ) {
Box( Box(
modifier = modifier =
modifier modifier
.minimumInteractiveComponentSize() .minimumInteractiveComponentSize()
.size(IconButtonTokens.StateLayerSize) .size(IconButtonTokens.StateLayerSize)
.clip(IconButtonTokens.StateLayerShape) .clip(IconButtonTokens.StateLayerShape)
.background(color = colors.containerColor(enabled)) .background(color = colors.containerColor(enabled))
.combinedClickable( .combinedClickable(
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
enabled = enabled, enabled = enabled,
interactionSource = interactionSource, interactionSource = interactionSource,
indication = rippleOrFallbackImplementation( indication = ripple()
bounded = false, ),
radius = IconButtonTokens.StateLayerSize / 2
)
),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
val contentColor = colors.contentColor(enabled) val contentColor = colors.contentColor(enabled)
@@ -61,21 +54,6 @@ fun IconButton(
} }
} }
@Suppress("DEPRECATION_ERROR")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun rippleOrFallbackImplementation(
bounded: Boolean = true,
radius: Dp = Dp.Unspecified,
color: Color = Color.Unspecified
): Indication {
return if (LocalUseFallbackRippleImplementation.current) {
rememberRipple(bounded, radius, color)
} else {
ripple(bounded, radius, color)
}
}
internal object IconButtonTokens { internal object IconButtonTokens {
val StateLayerShape = CircleShape val StateLayerShape = CircleShape
val StateLayerSize = 40.0.dp val StateLayerSize = 40.0.dp
@@ -1,11 +1,6 @@
package dev.meloda.fast.messageshistory.presentation package dev.meloda.fast.messageshistory.presentation
import android.os.Build
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.annotation.VisibleForTesting
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
@@ -41,6 +36,8 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.contextmenu.builder.item
import androidx.compose.foundation.text.contextmenu.modifier.addTextContextMenuComponents
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
@@ -62,7 +59,6 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -74,15 +70,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.TextToolbarStatus
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.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
@@ -149,6 +142,7 @@ fun MessagesHistoryScreen(
onItalicRequested: () -> Unit = {}, onItalicRequested: () -> Unit = {},
onUnderlineRequested: () -> Unit = {}, onUnderlineRequested: () -> Unit = {},
) { ) {
val context = LocalContext.current
val view = LocalView.current val view = LocalView.current
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val theme = LocalThemeConfig.current val theme = LocalThemeConfig.current
@@ -574,37 +568,59 @@ fun MessagesHistoryScreen(
} }
} }
val view = LocalView.current TextField(
val textToolbar = remember { modifier = Modifier
CustomTextToolbar( .weight(1f)
view = view, .addTextContextMenuComponents {
onBoldRequested = onBoldRequested, separator()
onItalicRequested = onItalicRequested,
onUnderlineRequested = onUnderlineRequested, item(
onLinkRequested = {} key = "Bold",
) label = context.getString(UiR.string.bold)
} ) {
onBoldRequested()
close()
}
item(
key = "Italic",
label = context.getString(UiR.string.italic)
) {
onItalicRequested()
close()
}
item(
key = "Underline",
label = context.getString(UiR.string.underline)
) {
onUnderlineRequested()
close()
}
item(
key = "Link",
label = context.getString(UiR.string.link)
) {
close()
}
separator()
},
value = screenState.message,
onValueChange = onMessageInputChanged,
colors = TextFieldDefaults.colors(
unfocusedContainerColor = Color.Transparent,
focusedContainerColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
),
placeholder = {
Text(
text = stringResource(id = UiR.string.message_input_hint),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
CompositionLocalProvider(LocalTextToolbar provides textToolbar) {
TextField(
modifier = Modifier.weight(1f),
value = screenState.message,
onValueChange = onMessageInputChanged,
colors = TextFieldDefaults.colors(
unfocusedContainerColor = Color.Transparent,
focusedContainerColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
),
placeholder = {
Text(
text = stringResource(id = UiR.string.message_input_hint),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
}
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val attachmentRotation = remember { Animatable(0f) } val attachmentRotation = remember { Animatable(0f) }
@@ -717,242 +733,3 @@ fun MessagesHistoryScreen(
} }
} }
} }
class CustomTextToolbar(
private val view: View,
private var onBoldRequested: (() -> Unit)? = null,
private var onItalicRequested: (() -> Unit)? = null,
private var onUnderlineRequested: (() -> Unit)? = null,
private var onLinkRequested: (() -> Unit)? = null
) : TextToolbar {
private var actionMode: android.view.ActionMode? = null
private val textActionModeCallback: TextActionModeCallback =
TextActionModeCallback(onActionModeDestroy = { actionMode = null })
override var status: TextToolbarStatus = TextToolbarStatus.Hidden
private set
override fun showMenu(
rect: Rect,
onCopyRequested: (() -> Unit)?,
onPasteRequested: (() -> Unit)?,
onCutRequested: (() -> Unit)?,
onSelectAllRequested: (() -> Unit)?,
onAutofillRequested: (() -> Unit)?
) {
textActionModeCallback.rect = rect
textActionModeCallback.onCopyRequested = onCopyRequested
textActionModeCallback.onCutRequested = onCutRequested
textActionModeCallback.onPasteRequested = onPasteRequested
textActionModeCallback.onSelectAllRequested = onSelectAllRequested
textActionModeCallback.onAutofillRequested = onAutofillRequested
textActionModeCallback.onBoldRequested = onBoldRequested
textActionModeCallback.onItalicRequested = onItalicRequested
textActionModeCallback.onUnderlineRequested = onUnderlineRequested
textActionModeCallback.onLinkRequested = onLinkRequested
if (actionMode == null) {
status = TextToolbarStatus.Shown
actionMode =
TextToolbarHelperMethods.startActionMode(
view,
FloatingTextActionModeCallback(textActionModeCallback),
android.view.ActionMode.TYPE_FLOATING
)
} else {
actionMode?.invalidate()
}
}
override fun showMenu(
rect: Rect,
onCopyRequested: (() -> Unit)?,
onPasteRequested: (() -> Unit)?,
onCutRequested: (() -> Unit)?,
onSelectAllRequested: (() -> Unit)?
) {
showMenu(
rect = rect,
onCopyRequested = onCopyRequested,
onPasteRequested = onPasteRequested,
onCutRequested = onCutRequested,
onSelectAllRequested = onSelectAllRequested,
onAutofillRequested = null
)
}
override fun hide() {
status = TextToolbarStatus.Hidden
actionMode?.finish()
actionMode = null
}
}
/**
* This class is here to ensure that the classes that use this API will get verified and can be AOT
* compiled. It is expected that this class will soft-fail verification, but the classes which use
* this method will pass.
*/
internal object TextToolbarHelperMethods {
fun startActionMode(
view: View,
actionModeCallback: android.view.ActionMode.Callback,
type: Int
): android.view.ActionMode? {
return view.startActionMode(actionModeCallback, type)
}
fun invalidateContentRect(actionMode: android.view.ActionMode) {
actionMode.invalidateContentRect()
}
}
class FloatingTextActionModeCallback(private val callback: TextActionModeCallback) :
android.view.ActionMode.Callback2() {
override fun onActionItemClicked(mode: android.view.ActionMode?, item: MenuItem?): Boolean {
return callback.onActionItemClicked(mode, item)
}
override fun onCreateActionMode(mode: android.view.ActionMode?, menu: Menu?): Boolean {
return callback.onCreateActionMode(mode, menu)
}
override fun onPrepareActionMode(mode: android.view.ActionMode?, menu: Menu?): Boolean {
return callback.onPrepareActionMode(mode, menu)
}
override fun onDestroyActionMode(mode: android.view.ActionMode?) {
callback.onDestroyActionMode()
}
override fun onGetContentRect(
mode: android.view.ActionMode?,
view: View?,
outRect: android.graphics.Rect?
) {
val rect = callback.rect
outRect?.set(rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt())
}
}
class TextActionModeCallback(
val onActionModeDestroy: (() -> Unit)? = null,
var rect: Rect = Rect.Zero,
var onCopyRequested: (() -> Unit)? = null,
var onPasteRequested: (() -> Unit)? = null,
var onCutRequested: (() -> Unit)? = null,
var onSelectAllRequested: (() -> Unit)? = null,
var onAutofillRequested: (() -> Unit)? = null,
var onBoldRequested: (() -> Unit)? = null,
var onItalicRequested: (() -> Unit)? = null,
var onUnderlineRequested: (() -> Unit)? = null,
var onLinkRequested: (() -> Unit)? = null
) {
fun onCreateActionMode(mode: android.view.ActionMode?, menu: Menu?): Boolean {
requireNotNull(menu) { "onCreateActionMode requires a non-null menu" }
requireNotNull(mode) { "onCreateActionMode requires a non-null mode" }
onCopyRequested?.let { addMenuItem(menu, MenuItemOption.Copy) }
onPasteRequested?.let { addMenuItem(menu, MenuItemOption.Paste) }
onCutRequested?.let { addMenuItem(menu, MenuItemOption.Cut) }
onSelectAllRequested?.let { addMenuItem(menu, MenuItemOption.SelectAll) }
if (onAutofillRequested != null && Build.VERSION.SDK_INT >= 26) {
addMenuItem(menu, MenuItemOption.Autofill)
}
onBoldRequested?.let { addMenuItem(menu, MenuItemOption.Bold) }
onItalicRequested?.let { addMenuItem(menu, MenuItemOption.Italic) }
onUnderlineRequested?.let { addMenuItem(menu, MenuItemOption.Underline) }
onLinkRequested?.let { addMenuItem(menu, MenuItemOption.Link) }
return true
}
// this method is called to populate new menu items when the actionMode was invalidated
fun onPrepareActionMode(mode: android.view.ActionMode?, menu: Menu?): Boolean {
if (mode == null || menu == null) return false
updateMenuItems(menu)
// should return true so that new menu items are populated
return true
}
fun onActionItemClicked(mode: android.view.ActionMode?, item: MenuItem?): Boolean {
when (item!!.itemId) {
MenuItemOption.Copy.ordinal -> onCopyRequested?.invoke()
MenuItemOption.Paste.ordinal -> onPasteRequested?.invoke()
MenuItemOption.Cut.ordinal -> onCutRequested?.invoke()
MenuItemOption.SelectAll.ordinal -> onSelectAllRequested?.invoke()
MenuItemOption.Autofill.ordinal -> onAutofillRequested?.invoke()
MenuItemOption.Bold.ordinal -> onBoldRequested?.invoke()
MenuItemOption.Italic.ordinal -> onItalicRequested?.invoke()
MenuItemOption.Underline.ordinal -> onUnderlineRequested?.invoke()
MenuItemOption.Link.ordinal -> onLinkRequested?.invoke()
else -> return false
}
mode?.finish()
return true
}
fun onDestroyActionMode() {
onActionModeDestroy?.invoke()
}
@VisibleForTesting
internal fun updateMenuItems(menu: Menu) {
addOrRemoveMenuItem(menu, MenuItemOption.Copy, onCopyRequested)
addOrRemoveMenuItem(menu, MenuItemOption.Paste, onPasteRequested)
addOrRemoveMenuItem(menu, MenuItemOption.Cut, onCutRequested)
addOrRemoveMenuItem(menu, MenuItemOption.SelectAll, onSelectAllRequested)
addOrRemoveMenuItem(menu, MenuItemOption.Autofill, onAutofillRequested)
addOrRemoveMenuItem(menu, MenuItemOption.Bold, onBoldRequested)
addOrRemoveMenuItem(menu, MenuItemOption.Italic, onItalicRequested)
addOrRemoveMenuItem(menu, MenuItemOption.Underline, onUnderlineRequested)
addOrRemoveMenuItem(menu, MenuItemOption.Link, onLinkRequested)
}
private fun addMenuItem(menu: Menu, item: MenuItemOption) {
menu
.add(0, item.ordinal, item.order, item.titleResource)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
private fun addOrRemoveMenuItem(menu: Menu, item: MenuItemOption, callback: (() -> Unit)?) {
when {
callback != null && menu.findItem(item.ordinal) == null -> addMenuItem(menu, item)
callback == null && menu.findItem(item.ordinal) != null -> menu.removeItem(item.ordinal)
}
}
}
internal enum class MenuItemOption {
Copy,
Paste,
Cut,
SelectAll,
Autofill,
Bold,
Italic,
Underline,
Link;
val titleResource: Int
get() =
when (this) {
Copy -> android.R.string.copy
Paste -> android.R.string.paste
Cut -> android.R.string.cut
SelectAll -> android.R.string.selectAll
Autofill ->
if (Build.VERSION.SDK_INT <= 26) {
UiR.string.autofill
} else {
android.R.string.autofill
}
Bold -> UiR.string.bold
Italic -> UiR.string.italic
Underline -> UiR.string.underline
Link -> UiR.string.link
}
/** This item will be shown before all items that have order greater than this value. */
val order = ordinal
}
@@ -33,8 +33,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.haze import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeChild import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
@@ -159,7 +159,7 @@ fun SettingsScreen(
modifier = Modifier modifier = Modifier
.then( .then(
if (themeConfig.enableBlur) { if (themeConfig.enableBlur) {
Modifier.hazeChild( Modifier.hazeEffect(
state = hazeState, state = hazeState,
style = HazeMaterials.thick() style = HazeMaterials.thick()
) )
@@ -175,7 +175,7 @@ fun SettingsScreen(
modifier = Modifier modifier = Modifier
.then( .then(
if (themeConfig.enableBlur) { if (themeConfig.enableBlur) {
Modifier.haze(state = hazeState) Modifier.hazeSource(state = hazeState)
} else Modifier } else Modifier
) )
.fillMaxWidth() .fillMaxWidth()
+4 -2
View File
@@ -12,7 +12,7 @@ haze = "1.6.4"
kotlin = "2.1.21" kotlin = "2.1.21"
ksp = "2.1.21-2.0.2" ksp = "2.1.21-2.0.2"
compose-bom = "2025.06.01" compose-bom = "2025.06.00"
koin = "4.1.0" koin = "4.1.0"
accompanist = "0.37.3" accompanist = "0.37.3"
@@ -68,7 +68,8 @@ nanokt-jvm = { module = "com.conena.nanokt:nanokt-jvm", version.ref = "nanokt" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } compose-bom = { module = "androidx.compose:compose-bom-alpha", version.ref = "compose-bom" }
compose-material-icons = { module = "androidx.compose.material:material-icons-core" }
compose-material3 = { module = "androidx.compose.material3:material3" } compose-material3 = { module = "androidx.compose.material3:material3" }
compose-ui = { module = "androidx.compose.ui:ui" } compose-ui = { module = "androidx.compose.ui:ui" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
@@ -95,6 +96,7 @@ room-gradlePlugin = { group = "androidx.room", name = "room-gradle-plugin", vers
[bundles] [bundles]
compose = [ compose = [
"compose-material-icons",
"compose-material3", "compose-material3",
"compose-material3-windowsize", "compose-material3-windowsize",
"compose-ui", "compose-ui",