forked from melod1n/fast-messenger
switch to compose-bom-alpha
This commit is contained in:
@@ -50,7 +50,6 @@ import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.theme.LocalUser
|
||||
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.KoinContext
|
||||
import org.koin.compose.koinInject
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@@ -89,169 +88,167 @@ class MainActivity : AppCompatActivity() {
|
||||
requestNotificationPermissions()
|
||||
|
||||
setContent {
|
||||
KoinContext {
|
||||
val context = LocalContext.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val userSettings: UserSettings = koinInject()
|
||||
val longPollController: LongPollController = koinInject()
|
||||
val userSettings: UserSettings = koinInject()
|
||||
val longPollController: LongPollController = koinInject()
|
||||
|
||||
val longPollCurrentState by longPollController.currentState.collectAsStateWithLifecycle()
|
||||
val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle()
|
||||
val longPollCurrentState by longPollController.currentState.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) {
|
||||
viewModel.onAppResumed(intent)
|
||||
onPauseOrDispose {}
|
||||
}
|
||||
LifecycleResumeEffect(true) {
|
||||
viewModel.onAppResumed(intent)
|
||||
onPauseOrDispose {}
|
||||
}
|
||||
|
||||
val permissionState =
|
||||
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
|
||||
val permissionState =
|
||||
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
||||
val isNeedToCheckPermission by viewModel.isNeedToCheckNotificationsPermission.collectAsStateWithLifecycle()
|
||||
val isNeedToRequestPermission by viewModel.isNeedToRequestNotifications.collectAsStateWithLifecycle()
|
||||
val isNeedToCheckPermission by viewModel.isNeedToCheckNotificationsPermission.collectAsStateWithLifecycle()
|
||||
val isNeedToRequestPermission by viewModel.isNeedToRequestNotifications.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(isNeedToCheckPermission) {
|
||||
if (isNeedToCheckPermission) {
|
||||
viewModel.onPermissionCheckStatus(permissionState.status)
|
||||
LaunchedEffect(isNeedToCheckPermission) {
|
||||
if (isNeedToCheckPermission) {
|
||||
viewModel.onPermissionCheckStatus(permissionState.status)
|
||||
|
||||
if (permissionState.status.isGranted) {
|
||||
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
|
||||
) {
|
||||
if (permissionState.status.isGranted) {
|
||||
if (longPollCurrentState == LongPollState.InApp) {
|
||||
toggleLongPollService(false)
|
||||
Log.d("LongPoll", "recreate()")
|
||||
}
|
||||
|
||||
toggleLongPollService(
|
||||
enable = longPollStateToApply.isLaunched(),
|
||||
inBackground = longPollStateToApply == LongPollState.Background
|
||||
enable = true,
|
||||
inBackground = true
|
||||
)
|
||||
}
|
||||
|
||||
onPauseOrDispose {}
|
||||
}
|
||||
}
|
||||
|
||||
val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle()
|
||||
LifecycleResumeEffect(sendOnline) {
|
||||
toggleOnlineService(sendOnline)
|
||||
LaunchedEffect(isNeedToRequestPermission) {
|
||||
if (isNeedToRequestPermission) {
|
||||
viewModel.onPermissionsRequested()
|
||||
permissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
|
||||
onPauseOrDispose {
|
||||
toggleOnlineService(false)
|
||||
LifecycleResumeEffect(longPollStateToApply) {
|
||||
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) {
|
||||
context.resources.displayMetrics.widthPixels.pxToDp()
|
||||
}
|
||||
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
|
||||
)
|
||||
toggleLongPollService(
|
||||
enable = longPollStateToApply.isLaunched(),
|
||||
inBackground = longPollStateToApply == LongPollState.Background
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
onPauseOrDispose {}
|
||||
}
|
||||
|
||||
val setDarkMode = isNeedToEnableDarkMode(darkMode = darkMode)
|
||||
val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle()
|
||||
LifecycleResumeEffect(sendOnline) {
|
||||
toggleOnlineService(sendOnline)
|
||||
|
||||
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
|
||||
)
|
||||
onPauseOrDispose {
|
||||
toggleOnlineService(false)
|
||||
}
|
||||
}
|
||||
|
||||
val deviceWidthDp = remember(true) {
|
||||
context.resources.displayMetrics.widthPixels.pxToDp()
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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()
|
||||
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
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Indication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.IconButtonColors
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalUseFallbackRippleImplementation
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -23,10 +20,9 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun IconButton(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -39,21 +35,18 @@ fun IconButton(
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.minimumInteractiveComponentSize()
|
||||
.size(IconButtonTokens.StateLayerSize)
|
||||
.clip(IconButtonTokens.StateLayerShape)
|
||||
.background(color = colors.containerColor(enabled))
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
enabled = enabled,
|
||||
interactionSource = interactionSource,
|
||||
indication = rippleOrFallbackImplementation(
|
||||
bounded = false,
|
||||
radius = IconButtonTokens.StateLayerSize / 2
|
||||
)
|
||||
),
|
||||
modifier
|
||||
.minimumInteractiveComponentSize()
|
||||
.size(IconButtonTokens.StateLayerSize)
|
||||
.clip(IconButtonTokens.StateLayerShape)
|
||||
.background(color = colors.containerColor(enabled))
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
enabled = enabled,
|
||||
interactionSource = interactionSource,
|
||||
indication = ripple()
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
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 {
|
||||
val StateLayerShape = CircleShape
|
||||
val StateLayerSize = 40.0.dp
|
||||
|
||||
+56
-279
@@ -1,11 +1,6 @@
|
||||
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.annotation.VisibleForTesting
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
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.shape.CircleShape
|
||||
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.automirrored.rounded.ArrowBack
|
||||
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.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -74,15 +70,12 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalTextToolbar
|
||||
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.stringResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
@@ -149,6 +142,7 @@ fun MessagesHistoryScreen(
|
||||
onItalicRequested: () -> Unit = {},
|
||||
onUnderlineRequested: () -> Unit = {},
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val view = LocalView.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val theme = LocalThemeConfig.current
|
||||
@@ -574,37 +568,59 @@ fun MessagesHistoryScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
val textToolbar = remember {
|
||||
CustomTextToolbar(
|
||||
view = view,
|
||||
onBoldRequested = onBoldRequested,
|
||||
onItalicRequested = onItalicRequested,
|
||||
onUnderlineRequested = onUnderlineRequested,
|
||||
onLinkRequested = {}
|
||||
)
|
||||
}
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.addTextContextMenuComponents {
|
||||
separator()
|
||||
|
||||
item(
|
||||
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 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
|
||||
}
|
||||
|
||||
+4
-4
@@ -33,8 +33,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.haze
|
||||
import dev.chrisbanes.haze.hazeChild
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.data.UserConfig
|
||||
@@ -159,7 +159,7 @@ fun SettingsScreen(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (themeConfig.enableBlur) {
|
||||
Modifier.hazeChild(
|
||||
Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick()
|
||||
)
|
||||
@@ -175,7 +175,7 @@ fun SettingsScreen(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (themeConfig.enableBlur) {
|
||||
Modifier.haze(state = hazeState)
|
||||
Modifier.hazeSource(state = hazeState)
|
||||
} else Modifier
|
||||
)
|
||||
.fillMaxWidth()
|
||||
|
||||
@@ -12,7 +12,7 @@ haze = "1.6.4"
|
||||
kotlin = "2.1.21"
|
||||
ksp = "2.1.21-2.0.2"
|
||||
|
||||
compose-bom = "2025.06.01"
|
||||
compose-bom = "2025.06.00"
|
||||
koin = "4.1.0"
|
||||
|
||||
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" }
|
||||
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-ui = { module = "androidx.compose.ui:ui" }
|
||||
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]
|
||||
compose = [
|
||||
"compose-material-icons",
|
||||
"compose-material3",
|
||||
"compose-material3-windowsize",
|
||||
"compose-ui",
|
||||
|
||||
Reference in New Issue
Block a user