wolfskin/app/src/main/java/com/github/damontecres/dolphin/Extensions.kt
2025-09-23 16:14:01 -04:00

110 lines
3.2 KiB
Kotlin

package com.github.damontecres.dolphin
import android.view.KeyEvent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.input.key.onPreviewKeyEvent
import kotlinx.coroutines.CoroutineScope
import timber.log.Timber
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
fun CharSequence?.isNotNullOrBlank(): Boolean {
contract {
returns(true) implies (this@isNotNullOrBlank != null)
}
return !this.isNullOrBlank()
}
/**
* Try to call [FocusRequester.requestFocus], but catch & log the exception if something is not configured properly
*/
fun FocusRequester.tryRequestFocus(): Boolean =
try {
requestFocus()
true
} catch (ex: IllegalStateException) {
Timber.w(ex, "Failed to request focus")
false
}
/**
* Used to apply modifiers conditionally.
*/
fun Modifier.ifElse(
condition: () -> Boolean,
ifTrueModifier: Modifier,
ifFalseModifier: Modifier = Modifier,
): Modifier = then(if (condition()) ifTrueModifier else ifFalseModifier)
/**
* Used to apply modifiers conditionally.
*/
fun Modifier.ifElse(
condition: Boolean,
ifTrueModifier: Modifier,
ifFalseModifier: Modifier = Modifier,
): Modifier = ifElse({ condition }, ifTrueModifier, ifFalseModifier)
/**
* Handles horizontal (Left & Right) D-Pad Keys and consumes the event(s) so that the focus doesn't
* accidentally move to another element.
* */
fun Modifier.handleDPadKeyEvents(
onLeft: (() -> Unit)? = null,
onRight: (() -> Unit)? = null,
onCenter: (() -> Unit)? = null,
triggerOnAction: Int = KeyEvent.ACTION_UP,
) = onPreviewKeyEvent {
fun onActionUp(block: () -> Unit) {
if (it.nativeKeyEvent.action == triggerOnAction) block()
}
when (it.nativeKeyEvent.keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT -> {
onLeft?.apply {
onActionUp(::invoke)
return@onPreviewKeyEvent true
}
}
KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT -> {
onRight?.apply {
onActionUp(::invoke)
return@onPreviewKeyEvent true
}
}
KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> {
onCenter?.apply {
onActionUp(::invoke)
return@onPreviewKeyEvent true
}
}
}
false
}
/**
* Run a [LaunchedEffect] exactly once even with multiple recompositions.
*
* If the composition is removed from the navigation back stack and "re-added", this will run again
*/
@Composable
fun OneTimeLaunchedEffect(runOnceBlock: suspend CoroutineScope.() -> Unit) {
var hasRun by rememberSaveable { mutableStateOf(false) }
if (!hasRun) {
LaunchedEffect(Unit) {
hasRun = true
runOnceBlock.invoke(this)
}
}
}