init projects
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
package ru.shadowsparky.ui.components
|
||||
|
||||
import android.app.UiModeManager
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
@Composable
|
||||
actual fun isTv(): Boolean {
|
||||
val context = LocalContext.current
|
||||
val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
|
||||
return uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package ru.shadowsparky.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
@Composable
|
||||
actual fun lightDynamicScheme(): ColorScheme? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
dynamicLightColorScheme(LocalContext.current)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun darkDynamicScheme(): ColorScheme? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
dynamicDarkColorScheme(LocalContext.current)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z" />
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M382,720L154,492L211,435L382,606L749,239L806,296L382,720Z" />
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M480,640Q555,640 607.5,587.5Q660,535 660,460Q660,385 607.5,332.5Q555,280 480,280Q405,280 352.5,332.5Q300,385 300,460Q300,535 352.5,587.5Q405,640 480,640ZM480,568Q435,568 403.5,536.5Q372,505 372,460Q372,415 403.5,383.5Q435,352 480,352Q525,352 556.5,383.5Q588,415 588,460Q588,505 556.5,536.5Q525,568 480,568ZM480,760Q334,760 214,678.5Q94,597 40,460Q94,323 214,241.5Q334,160 480,160Q626,160 746,241.5Q866,323 920,460Q866,597 746,678.5Q626,760 480,760ZM480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460ZM480,680Q593,680 687.5,620.5Q782,561 832,460Q782,359 687.5,299.5Q593,240 480,240Q367,240 272.5,299.5Q178,359 128,460Q178,561 272.5,620.5Q367,680 480,680Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M644,532L586,474Q595,427 559,386Q523,345 466,354L408,296Q425,288 442.5,284Q460,280 480,280Q555,280 607.5,332.5Q660,385 660,460Q660,480 656,497.5Q652,515 644,532ZM772,658L714,602Q752,573 781.5,538.5Q811,504 832,460Q782,359 688.5,299.5Q595,240 480,240Q451,240 423,244Q395,248 368,256L306,194Q347,177 390,168.5Q433,160 480,160Q631,160 749,243.5Q867,327 920,460Q897,519 859.5,569.5Q822,620 772,658ZM792,904L624,738Q589,749 553.5,754.5Q518,760 480,760Q329,760 211,676.5Q93,593 40,460Q61,407 93,361.5Q125,316 166,280L56,168L112,112L848,848L792,904ZM222,336Q193,362 169,393Q145,424 128,460Q178,561 271.5,620.5Q365,680 480,680Q500,680 519,677.5Q538,675 558,672L522,634Q511,637 501,638.5Q491,640 480,640Q405,640 352.5,587.5Q300,535 300,460Q300,449 301.5,439Q303,429 306,418L222,336ZM541,429L541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429ZM390,504Q390,504 390,504Q390,504 390,504L390,504Q390,504 390,504Q390,504 390,504Q390,504 390,504Q390,504 390,504Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,166 @@
|
||||
package ru.shadowsparky.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialExpressiveTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationRail
|
||||
import androidx.compose.material3.NavigationRailItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ru.shadowsparky.ui.nav.Route
|
||||
import ru.shadowsparky.ui.theme.ComponentDefaults
|
||||
import ru.shadowsparky.ui.theme.getColorScheme
|
||||
|
||||
const val USE_EXPERIMENTAL_RAIL = false
|
||||
|
||||
@Immutable
|
||||
data class NavigationEntry<out T : Route>(
|
||||
val route: T,
|
||||
val title: String,
|
||||
val painter: Painter
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BaseScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = TopAppBarDefaults.pinnedScrollBehavior(),
|
||||
snackbarState: SnackbarHostState? = null,
|
||||
colorScheme: ColorScheme = getColorScheme(),
|
||||
appBar: @Composable (scroll: TopAppBarScrollBehavior?) -> Unit,
|
||||
content: @Composable (padding: PaddingValues) -> Unit,
|
||||
) {
|
||||
BaseScreen<Route>(
|
||||
modifier,
|
||||
scrollBehavior,
|
||||
snackbarState,
|
||||
colorScheme,
|
||||
appBar = appBar,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun <T : Route> BaseScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = TopAppBarDefaults.pinnedScrollBehavior(),
|
||||
snackbarState: SnackbarHostState? = null,
|
||||
colorScheme: ColorScheme = getColorScheme(),
|
||||
currentRoute: T? = null,
|
||||
navigationEntries: List<NavigationEntry<T>>? = null,
|
||||
onSelectNavigation: ((T) -> Unit)? = null,
|
||||
appBar: @Composable (scroll: TopAppBarScrollBehavior?) -> Unit,
|
||||
content: @Composable (padding: PaddingValues) -> Unit,
|
||||
) {
|
||||
val windowType = currentWindowType()
|
||||
MaterialExpressiveTheme(colorScheme = colorScheme) {
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
if (windowType.isCompact || !USE_EXPERIMENTAL_RAIL) {
|
||||
BottomBar(navigationEntries, currentRoute, onSelectNavigation)
|
||||
}
|
||||
},
|
||||
topBar = {
|
||||
appBar(scrollBehavior)
|
||||
}, content = {
|
||||
if (windowType.isExpanded && navigationEntries?.isNotEmpty() == true && USE_EXPERIMENTAL_RAIL) {
|
||||
ExpandedContent(
|
||||
it,
|
||||
navigationEntries,
|
||||
currentRoute,
|
||||
onSelectNavigation,
|
||||
content
|
||||
)
|
||||
} else {
|
||||
content(it)
|
||||
}
|
||||
}, modifier = if (scrollBehavior != null) {
|
||||
modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
} else {
|
||||
modifier
|
||||
}, snackbarHost = {
|
||||
snackbarState?.let { SnackbarHost(it) }
|
||||
}, contentWindowInsets = WindowInsets.safeDrawing
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : Route> BottomBar(
|
||||
navigationEntries: List<NavigationEntry<T>>?,
|
||||
currentRoute: T?,
|
||||
onSelectNavigation: ((T) -> Unit)?
|
||||
) {
|
||||
val blurState = LocalBlurState.current
|
||||
navigationEntries?.let {
|
||||
NavigationBar(
|
||||
containerColor = ComponentDefaults.navBarContentColor(),
|
||||
modifier = Modifier.applyBlurEffect(blurState)
|
||||
) {
|
||||
it.forEach { entry ->
|
||||
NavigationBarItem(
|
||||
currentRoute == entry.route,
|
||||
onClick = { onSelectNavigation?.invoke(entry.route) },
|
||||
icon = {
|
||||
Icon(entry.painter, contentDescription = entry.title)
|
||||
},
|
||||
label = { Text(entry.title) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun <T : Route> ExpandedContent(
|
||||
values: PaddingValues,
|
||||
navigationEntries: List<NavigationEntry<T>>,
|
||||
currentRoute: T?,
|
||||
onSelectNavigation: ((T) -> Unit)?,
|
||||
content: @Composable ((PaddingValues) -> Unit)
|
||||
) {
|
||||
Row(Modifier.fillMaxSize().padding(values)) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
NavigationRail {
|
||||
navigationEntries.forEach { entry ->
|
||||
NavigationRailItem(
|
||||
selected = currentRoute == entry.route,
|
||||
onClick = { onSelectNavigation?.invoke(entry.route) },
|
||||
icon = {
|
||||
Icon(entry.painter, contentDescription = entry.title)
|
||||
},
|
||||
label = { Text(entry.title) }
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Box(Modifier.weight(1f)) {
|
||||
content(PaddingValues(0.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package ru.shadowsparky.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.chrisbanes.haze.rememberHazeState
|
||||
import ru.shadowsparky.ui.components.isTv
|
||||
|
||||
private const val BLUR_ENABLED = true
|
||||
|
||||
val LocalBlurState = compositionLocalOf<BlurState?> { null }
|
||||
|
||||
@Composable
|
||||
internal fun isBlurEnabled(): Boolean {
|
||||
return BLUR_ENABLED && !isTv()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberBlurState(): BlurState? {
|
||||
if (!isBlurEnabled()) return null
|
||||
val hazeState = rememberHazeState()
|
||||
return remember(hazeState) { BlurState(hazeState) }
|
||||
}
|
||||
|
||||
fun Modifier.applyBlurSource(state: BlurState?): Modifier {
|
||||
state ?: return this
|
||||
return hazeSource(state.hazeState)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalHazeMaterialsApi::class)
|
||||
@Composable
|
||||
fun Modifier.applyBlurEffect(state: BlurState?): Modifier {
|
||||
state ?: return this
|
||||
return hazeEffect(state.hazeState, HazeMaterials.thick())
|
||||
}
|
||||
|
||||
@Stable
|
||||
class BlurState(internal val hazeState: HazeState)
|
||||
@@ -0,0 +1,30 @@
|
||||
package ru.shadowsparky.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
|
||||
@Composable
|
||||
private fun currentWindowWidthDp(): Int {
|
||||
val info = LocalWindowInfo.current
|
||||
val density = LocalDensity.current
|
||||
return with(density) { info.containerSize.width.toDp().value.toInt() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun currentWindowType(): WindowType {
|
||||
return widthToSizeClass(currentWindowWidthDp())
|
||||
}
|
||||
|
||||
sealed interface WindowType {
|
||||
object Compact : WindowType
|
||||
object Expanded : WindowType
|
||||
}
|
||||
|
||||
val WindowType.isExpanded: Boolean get() = this is WindowType.Expanded
|
||||
val WindowType.isCompact: Boolean get() = this is WindowType.Compact
|
||||
|
||||
private fun widthToSizeClass(widthDp: Int): WindowType = when {
|
||||
widthDp >= 840 -> WindowType.Expanded
|
||||
else -> WindowType.Compact
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package ru.shadowsparky.ui.components
|
||||
|
||||
import androiddev.libs.ui.generated.resources.Res
|
||||
import androiddev.libs.ui.generated.resources.back
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
@Composable
|
||||
fun BackButton(doBack: () -> Unit) {
|
||||
IconButton(
|
||||
onClick = { doBack() },
|
||||
shape = CircleShape,
|
||||
colors = IconButtonDefaults.iconButtonVibrantColors(containerColor = MaterialTheme.colorScheme.primaryContainer.copy(0.1f))
|
||||
) { Icon(painterResource(Res.drawable.back), contentDescription = null) }
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package ru.shadowsparky.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
private const val ROUNDED_MAX = 24
|
||||
private const val ROUNDED_MIN = 6
|
||||
|
||||
@DslMarker private annotation class ExpressiveListDsl
|
||||
|
||||
internal data class ExpressiveNode(
|
||||
val key: Any?,
|
||||
val contentType: Any?,
|
||||
val content: @Composable () -> Unit
|
||||
)
|
||||
|
||||
@ExpressiveListDsl
|
||||
class ExpressiveSectionScope {
|
||||
internal val nodes = mutableListOf<ExpressiveNode>()
|
||||
|
||||
fun item(key: Any? = null, contentType: Any? = null, content: @Composable () -> Unit) {
|
||||
nodes += ExpressiveNode(key, contentType, content)
|
||||
}
|
||||
|
||||
fun <T> items(
|
||||
items: List<T>,
|
||||
key: ((T) -> Any)? = null,
|
||||
contentType: ((T) -> Any)? = null,
|
||||
itemContent: @Composable (T) -> Unit
|
||||
) {
|
||||
items.forEach { t ->
|
||||
nodes += ExpressiveNode(
|
||||
key = key?.invoke(t),
|
||||
contentType = contentType?.invoke(t),
|
||||
content = { itemContent(t) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExpressiveListDsl
|
||||
class ExpressiveLazyListScope {
|
||||
internal val sections = mutableListOf<ExpressiveSectionScope>()
|
||||
|
||||
fun section(content: ExpressiveSectionScope.() -> Unit) {
|
||||
val s = ExpressiveSectionScope().apply(content)
|
||||
if (s.nodes.isNotEmpty()) sections += s
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExpressiveLazyColumn(
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
horizontalPadding: Dp = 8.dp,
|
||||
verticalSpacing: Dp = 1.dp,
|
||||
sectionSpacing: Dp = 8.dp,
|
||||
roundedMax: Dp = ROUNDED_MAX.dp,
|
||||
roundedMin: Dp = ROUNDED_MIN.dp,
|
||||
reverseLayout: Boolean = false,
|
||||
verticalArrangement: Arrangement.Vertical =
|
||||
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
|
||||
content: ExpressiveLazyListScope.() -> Unit
|
||||
) {
|
||||
val scope = ExpressiveLazyListScope().apply(content)
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding,
|
||||
reverseLayout = reverseLayout,
|
||||
verticalArrangement = verticalArrangement
|
||||
) {
|
||||
scope.sections.forEachIndexed { sectionIndex, section ->
|
||||
|
||||
if (sectionIndex != 0 && sectionSpacing > 0.dp) {
|
||||
item(key = "sectionSpacer_$sectionIndex") {
|
||||
Spacer(Modifier.height(sectionSpacing))
|
||||
}
|
||||
}
|
||||
|
||||
section.nodes.forEachIndexed { index, node ->
|
||||
val topRadius = if (index == 0) roundedMax else roundedMin
|
||||
val bottomRadius = if (index == section.nodes.lastIndex) roundedMax else roundedMin
|
||||
|
||||
item(key = node.key, contentType = node.contentType) {
|
||||
ExpressiveCard(
|
||||
topRadius,
|
||||
bottomRadius,
|
||||
horizontalPadding,
|
||||
verticalSpacing,
|
||||
content = node.content
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExpressiveCard(
|
||||
topRadius: Dp = ROUNDED_MAX.dp,
|
||||
bottomRadius: Dp = ROUNDED_MAX.dp,
|
||||
horizontalPadding: Dp = 8.dp,
|
||||
verticalSpacing: Dp = 1.dp,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
),
|
||||
shape = RoundedCornerShape(
|
||||
topStart = topRadius,
|
||||
topEnd = topRadius,
|
||||
bottomStart = bottomRadius,
|
||||
bottomEnd = bottomRadius
|
||||
),
|
||||
modifier = modifier.padding(horizontal = horizontalPadding, vertical = verticalSpacing)
|
||||
) { content() }
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package ru.shadowsparky.ui.components
|
||||
|
||||
import androidx.compose.foundation.OverscrollEffect
|
||||
import androidx.compose.foundation.gestures.FlingBehavior
|
||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberOverscrollEffect
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
expect fun isTv(): Boolean
|
||||
|
||||
/**
|
||||
* LazyColumn — обходной путь для бага LazyColumn + contentPadding на Android TV.
|
||||
*
|
||||
* Ниже — визуальные схемы, которые показывают,
|
||||
* КАК ломается фокус и КАК работает обход.
|
||||
*
|
||||
* ------------------------------------------------------------
|
||||
* 1) Как работает обычный LazyColumn с contentPadding
|
||||
* (и почему это ломает DPAD-фокус на Android TV)
|
||||
* ------------------------------------------------------------
|
||||
*
|
||||
* ┌───────────────────────────────────────────────┐
|
||||
* │ LazyColumn(contentPadding = paddingValues) │
|
||||
* │ │
|
||||
* │ ↑ ВЕРХНИЙ PADDING (невидимая область) │
|
||||
* │ │ (создаётся внутри списка) │
|
||||
* │ │ │
|
||||
* │ ┌─▼─────────────────────────────────────────┐ │
|
||||
* │ │ Item 1 │ │
|
||||
* │ └───────────────────────────────────────────┘ │
|
||||
* │ ┌────────────────────────────────────────────┐│
|
||||
* │ │ Item 2 (кнопка, карточка, что угодно) ││
|
||||
* │ └────────────────────────────────────────────┘│
|
||||
* │ │
|
||||
* │ ↓ НИЖНИЙ PADDING (ещё одна невидимая зона) │
|
||||
* │ │
|
||||
* └───────────────────────────────────────────────┘
|
||||
*
|
||||
* ПРОБЛЕМА:
|
||||
* Для DPAD-навигации эти зоны — не просто отступы.
|
||||
* Это ПУСТЫЕ ОБЛАСТИ, которые участвуют в алгоритме Focus Search.
|
||||
*
|
||||
* Алгоритм думает так:
|
||||
*
|
||||
* "Нажали вниз → ниже есть пространство? Да. Есть фокусируемый элемент? Нет."
|
||||
*
|
||||
* В итоге фокус:
|
||||
* - не двигается вниз,
|
||||
* - может даже перестать попадать в список,
|
||||
* - прокрутка вниз не инициируется.
|
||||
*
|
||||
*
|
||||
* ------------------------------------------------------------
|
||||
* 2) Что делает InsetLazyColumn на Android TV
|
||||
* ------------------------------------------------------------
|
||||
*
|
||||
* Вместо внутреннего contentPadding мы делаем:
|
||||
*
|
||||
* modifier.padding(contentPadding)
|
||||
*
|
||||
* А внутрь LazyColumn передаём:
|
||||
*
|
||||
* contentPadding = PaddingValues(0.dp)
|
||||
*
|
||||
*
|
||||
* ┌───────────────────────────────────────────────┐
|
||||
* │ Modifier.padding(...) ← PADDING СНАРУЖИ │
|
||||
* │ │
|
||||
* │ ┌───────────────────────────────────────────┐ │
|
||||
* │ │ LazyColumn(contentPadding = 0.dp) │ │
|
||||
* │ │ │ │
|
||||
* │ │ Item 1 │ │
|
||||
* │ │ Item 2 │ │
|
||||
* │ │ Item 3 │ │
|
||||
* │ │ │ │
|
||||
* │ └───────────────────────────────────────────┘ │
|
||||
* │ │
|
||||
* └───────────────────────────────────────────────┘
|
||||
*
|
||||
* РЕЗУЛЬТАТ:
|
||||
*
|
||||
* ✔ Визуально всё выглядит так же (insets соблюдены).
|
||||
* ✔ Внутри LazyColumn нет искусственных «дыр».
|
||||
* ✔ DPAD-навигация работает корректно.
|
||||
* ✔ Список правильно скроллится вниз при фокусе.
|
||||
* ✔ Алгоритм Focus Search видит только настоящие элементы.
|
||||
*
|
||||
*
|
||||
* ------------------------------------------------------------
|
||||
* 3) Итоговая мысль
|
||||
* ------------------------------------------------------------
|
||||
*
|
||||
* Весь баг сводится к тому, что LazyColumn на TV неправильно
|
||||
* интерпретирует внутренний contentPadding как "дополнительные"
|
||||
* focus-space зоны, и DPAD начинает вести себя некорректно.
|
||||
*
|
||||
* Перенос padding наружу через Modifier полностью устраняет проблему,
|
||||
* сохраняя визуальное поведение.
|
||||
*/
|
||||
@Composable
|
||||
fun LazyColumn(
|
||||
modifier: Modifier = Modifier,
|
||||
state: LazyListState = rememberLazyListState(),
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
reverseLayout: Boolean = false,
|
||||
verticalArrangement: Arrangement.Vertical =
|
||||
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
userScrollEnabled: Boolean = true,
|
||||
overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(),
|
||||
content: LazyListScope.() -> Unit,
|
||||
) {
|
||||
val isTv = isTv()
|
||||
androidx.compose.foundation.lazy.LazyColumn(
|
||||
if (isTv) modifier.padding(contentPadding) else modifier,
|
||||
state,
|
||||
if (isTv) PaddingValues(0.dp) else contentPadding,
|
||||
reverseLayout,
|
||||
verticalArrangement,
|
||||
horizontalAlignment,
|
||||
flingBehavior,
|
||||
userScrollEnabled,
|
||||
overscrollEffect,
|
||||
content
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package ru.shadowsparky.ui.components
|
||||
|
||||
import androidx.compose.foundation.OverscrollEffect
|
||||
import androidx.compose.foundation.gestures.FlingBehavior
|
||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridScope
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.rememberOverscrollEffect
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun LazyVerticalGrid(
|
||||
columns: GridCells,
|
||||
modifier: Modifier = Modifier,
|
||||
state: LazyGridState = rememberLazyGridState(),
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
reverseLayout: Boolean = false,
|
||||
verticalArrangement: Arrangement.Vertical =
|
||||
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
|
||||
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
|
||||
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
userScrollEnabled: Boolean = true,
|
||||
overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(),
|
||||
content: LazyGridScope.() -> Unit,
|
||||
) {
|
||||
val isTv = isTv()
|
||||
androidx.compose.foundation.lazy.grid.LazyVerticalGrid(
|
||||
columns,
|
||||
if (isTv) modifier.padding(contentPadding) else modifier,
|
||||
state,
|
||||
if (isTv) PaddingValues(0.dp) else contentPadding,
|
||||
reverseLayout,
|
||||
verticalArrangement,
|
||||
horizontalArrangement,
|
||||
flingBehavior,
|
||||
userScrollEnabled,
|
||||
overscrollEffect,
|
||||
content
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package ru.shadowsparky.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun <T> ListPreference(
|
||||
title: String,
|
||||
items: Map<T, String>,
|
||||
selectedItem: T,
|
||||
onItemSelected: (T) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
val entries = remember(items) { items.entries.toList() }
|
||||
val selectedLabel = remember(items, selectedItem) { items[selectedItem].orEmpty() }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { if (enabled) expanded = !expanded },
|
||||
modifier = modifier
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selectedLabel,
|
||||
onValueChange = {}, // readOnly
|
||||
readOnly = true,
|
||||
label = { Text(title) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier
|
||||
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled)
|
||||
.fillMaxWidth(),
|
||||
enabled = enabled,
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded && enabled,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
entries.forEach { (value, label) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(label) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
if (value != selectedItem) onItemSelected(value)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package ru.shadowsparky.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.LoadingIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun Loading(modifier: Modifier) {
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
LoadingIndicator(Modifier.size(96.dp))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package ru.shadowsparky.ui.components
|
||||
|
||||
import androiddev.libs.ui.generated.resources.Res
|
||||
import androiddev.libs.ui.generated.resources.visibility
|
||||
import androiddev.libs.ui.generated.resources.visibility_off
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.jetbrains.compose.resources.vectorResource
|
||||
|
||||
@Composable
|
||||
fun PasswordTextField(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
labelRes: StringResource,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
onDone: (() -> Unit)? = null
|
||||
) {
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (onDone == null) ImeAction.Next else ImeAction.Done,
|
||||
keyboardType = KeyboardType.Password
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { onDone?.invoke() }),
|
||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
label = { Text(stringResource(labelRes)) },
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
enabled = enabled,
|
||||
trailingIcon = {
|
||||
val image = if (passwordVisible) {
|
||||
Res.drawable.visibility
|
||||
} else {
|
||||
Res.drawable.visibility_off
|
||||
}
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(imageVector = vectorResource(image), contentDescription = null)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package ru.shadowsparky.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun PreferenceCategory(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = modifier
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
top = 24.dp,
|
||||
end = 16.dp,
|
||||
bottom = 8.dp,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package ru.shadowsparky.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
||||
@Composable
|
||||
fun SwitchPreference(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
summary: String? = null,
|
||||
state: Boolean,
|
||||
onStateChanged: (Boolean) -> Unit
|
||||
) {
|
||||
ListItem(
|
||||
colors = ListItemDefaults.colors(Color.Transparent),
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = state,
|
||||
role = Role.Switch,
|
||||
onValueChange = onStateChanged
|
||||
),
|
||||
headlineContent = {
|
||||
Text(
|
||||
text,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
},
|
||||
supportingContent = summary?.let {
|
||||
{
|
||||
Text(
|
||||
it,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = state,
|
||||
onCheckedChange = null
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package ru.shadowsparky.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
||||
@Composable
|
||||
fun TextPreference(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
summary: String? = null
|
||||
) {
|
||||
ListItem(
|
||||
colors = ListItemDefaults.colors(Color.Transparent),
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
headlineContent = {
|
||||
Text(
|
||||
text,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
},
|
||||
supportingContent = summary?.let {
|
||||
{
|
||||
Text(
|
||||
it,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package ru.shadowsparky.ui.nav
|
||||
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.bringToFront
|
||||
import com.arkivanov.decompose.router.stack.childStack
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.router.stack.pushNew
|
||||
import com.arkivanov.decompose.router.stack.replaceAll
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.arkivanov.essenty.backhandler.BackHandlerOwner
|
||||
import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.KSerializer
|
||||
import org.koin.core.Koin
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
abstract class BaseRootComponent<R : Route>(
|
||||
private val componentContext: ComponentContext,
|
||||
) : ComponentContext by componentContext, BackHandlerOwner {
|
||||
protected abstract val initStack: List<R>
|
||||
protected abstract val serializer: KSerializer<R>
|
||||
protected abstract val koin: Koin
|
||||
|
||||
private val scope = coroutineScope()
|
||||
|
||||
protected val nav = StackNavigation<R>()
|
||||
|
||||
protected val childStack by lazy {
|
||||
childStack(
|
||||
source = nav,
|
||||
serializer = serializer,
|
||||
initialStack = { initStack },
|
||||
childFactory = ::newChild
|
||||
)
|
||||
}
|
||||
|
||||
private val _onFinish = MutableStateFlow<Unit?>(null)
|
||||
val onFinish = _onFinish.filterNotNull()
|
||||
|
||||
fun navReplace(vararg route: R) {
|
||||
nav.replaceAll(*route)
|
||||
}
|
||||
|
||||
fun nav(route: R, bringToFront: Boolean = false) {
|
||||
if (bringToFront) {
|
||||
nav.bringToFront(route)
|
||||
} else {
|
||||
nav.pushNew(route)
|
||||
}
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
nav.pop { popped ->
|
||||
if (!popped) finish()
|
||||
}
|
||||
}
|
||||
|
||||
fun finish() {
|
||||
scope.launch { _onFinish.emit(Unit) }
|
||||
}
|
||||
|
||||
val valueChildStack: Value<ChildStack<*, Child>> by lazy { childStack }
|
||||
|
||||
protected open fun newChild(route: Route, ctx: ComponentContext): Child {
|
||||
val factory = route.provideFactory<Child, Route>(koin)
|
||||
return factory.create(route, ctx, this as BaseRootComponent<Route>)
|
||||
}
|
||||
|
||||
private fun <T : Child, R : Route> Route.provideFactory(koin: Koin): ComponentFactory<T, R, Route> {
|
||||
return koin.get<ComponentFactory<T, R, Route>>(named(routeId))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package ru.shadowsparky.ui.nav
|
||||
|
||||
interface Child
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.shadowsparky.ui.nav
|
||||
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
|
||||
interface ComponentFactory<T : Child, R : Route, BaseRoute : Route> {
|
||||
fun create(route: R, child: ComponentContext, root: BaseRootComponent<BaseRoute>): T
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package ru.shadowsparky.ui.nav
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.arkivanov.decompose.ExperimentalDecomposeApi
|
||||
import com.arkivanov.decompose.extensions.compose.stack.Children
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.androidPredictiveBackAnimatableV2
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.predictiveBackAnimation
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
|
||||
|
||||
@OptIn(ExperimentalDecomposeApi::class)
|
||||
@Composable
|
||||
fun Nav(
|
||||
rootComponent: BaseRootComponent<*>,
|
||||
process: @Composable (child: Child, route: Route) -> Unit
|
||||
) {
|
||||
Children(
|
||||
stack = rootComponent.valueChildStack,
|
||||
animation = predictiveBackAnimation(
|
||||
backHandler = rootComponent.backHandler,
|
||||
fallbackAnimation = stackAnimation(fade()),
|
||||
onBack = rootComponent::onBack,
|
||||
selector = { backEvent, _, _ -> androidPredictiveBackAnimatableV2(backEvent) },
|
||||
),
|
||||
) {
|
||||
process(it.instance, it.configuration as Route)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package ru.shadowsparky.ui.nav
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
abstract class Route {
|
||||
abstract val routeId: String
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package ru.shadowsparky.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val primaryLight = Color(0xFF465D91)
|
||||
val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
val primaryContainerLight = Color(0xFFD9E2FF)
|
||||
val onPrimaryContainerLight = Color(0xFF2D4678)
|
||||
val secondaryLight = Color(0xFF575E71)
|
||||
val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
val secondaryContainerLight = Color(0xFFDBE2F9)
|
||||
val onSecondaryContainerLight = Color(0xFF3F4759)
|
||||
val tertiaryLight = Color(0xFF725573)
|
||||
val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
val tertiaryContainerLight = Color(0xFFFCD7FB)
|
||||
val onTertiaryContainerLight = Color(0xFF593E5A)
|
||||
val errorLight = Color(0xFFBA1A1A)
|
||||
val onErrorLight = Color(0xFFFFFFFF)
|
||||
val errorContainerLight = Color(0xFFFFDAD6)
|
||||
val onErrorContainerLight = Color(0xFF93000A)
|
||||
val backgroundLight = Color(0xFFFAF9FF)
|
||||
val onBackgroundLight = Color(0xFF1A1B20)
|
||||
val surfaceLight = Color(0xFFFAF9FF)
|
||||
val onSurfaceLight = Color(0xFF1A1B20)
|
||||
val surfaceVariantLight = Color(0xFFE1E2EC)
|
||||
val onSurfaceVariantLight = Color(0xFF44464F)
|
||||
val outlineLight = Color(0xFF757780)
|
||||
val outlineVariantLight = Color(0xFFC5C6D0)
|
||||
val scrimLight = Color(0xFF000000)
|
||||
val inverseSurfaceLight = Color(0xFF2F3036)
|
||||
val inverseOnSurfaceLight = Color(0xFFF1F0F7)
|
||||
val inversePrimaryLight = Color(0xFFAFC6FF)
|
||||
val surfaceDimLight = Color(0xFFDAD9E0)
|
||||
val surfaceBrightLight = Color(0xFFFAF9FF)
|
||||
val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||
val surfaceContainerLowLight = Color(0xFFF3F3FA)
|
||||
val surfaceContainerLight = Color(0xFFEEEDF4)
|
||||
val surfaceContainerHighLight = Color(0xFFE8E7EF)
|
||||
val surfaceContainerHighestLight = Color(0xFFE2E2E9)
|
||||
|
||||
val primaryLightMediumContrast = Color(0xFF1A3566)
|
||||
val onPrimaryLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val primaryContainerLightMediumContrast = Color(0xFF556CA1)
|
||||
val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val secondaryLightMediumContrast = Color(0xFF2F3648)
|
||||
val onSecondaryLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val secondaryContainerLightMediumContrast = Color(0xFF666D80)
|
||||
val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryLightMediumContrast = Color(0xFF472D49)
|
||||
val onTertiaryLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryContainerLightMediumContrast = Color(0xFF816382)
|
||||
val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val errorLightMediumContrast = Color(0xFF740006)
|
||||
val onErrorLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val errorContainerLightMediumContrast = Color(0xFFCF2C27)
|
||||
val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val backgroundLightMediumContrast = Color(0xFFFAF9FF)
|
||||
val onBackgroundLightMediumContrast = Color(0xFF1A1B20)
|
||||
val surfaceLightMediumContrast = Color(0xFFFAF9FF)
|
||||
val onSurfaceLightMediumContrast = Color(0xFF0F1116)
|
||||
val surfaceVariantLightMediumContrast = Color(0xFFE1E2EC)
|
||||
val onSurfaceVariantLightMediumContrast = Color(0xFF34363E)
|
||||
val outlineLightMediumContrast = Color(0xFF50525A)
|
||||
val outlineVariantLightMediumContrast = Color(0xFF6B6D75)
|
||||
val scrimLightMediumContrast = Color(0xFF000000)
|
||||
val inverseSurfaceLightMediumContrast = Color(0xFF2F3036)
|
||||
val inverseOnSurfaceLightMediumContrast = Color(0xFFF1F0F7)
|
||||
val inversePrimaryLightMediumContrast = Color(0xFFAFC6FF)
|
||||
val surfaceDimLightMediumContrast = Color(0xFFC6C6CD)
|
||||
val surfaceBrightLightMediumContrast = Color(0xFFFAF9FF)
|
||||
val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val surfaceContainerLowLightMediumContrast = Color(0xFFF3F3FA)
|
||||
val surfaceContainerLightMediumContrast = Color(0xFFE8E7EF)
|
||||
val surfaceContainerHighLightMediumContrast = Color(0xFFDCDCE3)
|
||||
val surfaceContainerHighestLightMediumContrast = Color(0xFFD1D1D8)
|
||||
|
||||
val primaryLightHighContrast = Color(0xFF0D2A5B)
|
||||
val onPrimaryLightHighContrast = Color(0xFFFFFFFF)
|
||||
val primaryContainerLightHighContrast = Color(0xFF30487B)
|
||||
val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val secondaryLightHighContrast = Color(0xFF252C3D)
|
||||
val onSecondaryLightHighContrast = Color(0xFFFFFFFF)
|
||||
val secondaryContainerLightHighContrast = Color(0xFF42495B)
|
||||
val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryLightHighContrast = Color(0xFF3C233E)
|
||||
val onTertiaryLightHighContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryContainerLightHighContrast = Color(0xFF5B405D)
|
||||
val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val errorLightHighContrast = Color(0xFF600004)
|
||||
val onErrorLightHighContrast = Color(0xFFFFFFFF)
|
||||
val errorContainerLightHighContrast = Color(0xFF98000A)
|
||||
val onErrorContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val backgroundLightHighContrast = Color(0xFFFAF9FF)
|
||||
val onBackgroundLightHighContrast = Color(0xFF1A1B20)
|
||||
val surfaceLightHighContrast = Color(0xFFFAF9FF)
|
||||
val onSurfaceLightHighContrast = Color(0xFF000000)
|
||||
val surfaceVariantLightHighContrast = Color(0xFFE1E2EC)
|
||||
val onSurfaceVariantLightHighContrast = Color(0xFF000000)
|
||||
val outlineLightHighContrast = Color(0xFF292C33)
|
||||
val outlineVariantLightHighContrast = Color(0xFF474951)
|
||||
val scrimLightHighContrast = Color(0xFF000000)
|
||||
val inverseSurfaceLightHighContrast = Color(0xFF2F3036)
|
||||
val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)
|
||||
val inversePrimaryLightHighContrast = Color(0xFFAFC6FF)
|
||||
val surfaceDimLightHighContrast = Color(0xFFB8B8BF)
|
||||
val surfaceBrightLightHighContrast = Color(0xFFFAF9FF)
|
||||
val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)
|
||||
val surfaceContainerLowLightHighContrast = Color(0xFFF1F0F7)
|
||||
val surfaceContainerLightHighContrast = Color(0xFFE2E2E9)
|
||||
val surfaceContainerHighLightHighContrast = Color(0xFFD4D4DB)
|
||||
val surfaceContainerHighestLightHighContrast = Color(0xFFC6C6CD)
|
||||
|
||||
val primaryDark = Color(0xFFAFC6FF)
|
||||
val onPrimaryDark = Color(0xFF132F60)
|
||||
val primaryContainerDark = Color(0xFF2D4678)
|
||||
val onPrimaryContainerDark = Color(0xFFD9E2FF)
|
||||
val secondaryDark = Color(0xFFBFC6DC)
|
||||
val onSecondaryDark = Color(0xFF293042)
|
||||
val secondaryContainerDark = Color(0xFF3F4759)
|
||||
val onSecondaryContainerDark = Color(0xFFDBE2F9)
|
||||
val tertiaryDark = Color(0xFFDFBBDE)
|
||||
val onTertiaryDark = Color(0xFF412743)
|
||||
val tertiaryContainerDark = Color(0xFF593E5A)
|
||||
val onTertiaryContainerDark = Color(0xFFFCD7FB)
|
||||
val errorDark = Color(0xFFFFB4AB)
|
||||
val onErrorDark = Color(0xFF690005)
|
||||
val errorContainerDark = Color(0xFF93000A)
|
||||
val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||
val backgroundDark = Color(0xFF121318)
|
||||
val onBackgroundDark = Color(0xFFE2E2E9)
|
||||
val surfaceDark = Color(0xFF121318)
|
||||
val onSurfaceDark = Color(0xFFE2E2E9)
|
||||
val surfaceVariantDark = Color(0xFF44464F)
|
||||
val onSurfaceVariantDark = Color(0xFFC5C6D0)
|
||||
val outlineDark = Color(0xFF8F9099)
|
||||
val outlineVariantDark = Color(0xFF44464F)
|
||||
val scrimDark = Color(0xFF000000)
|
||||
val inverseSurfaceDark = Color(0xFFE2E2E9)
|
||||
val inverseOnSurfaceDark = Color(0xFF2F3036)
|
||||
val inversePrimaryDark = Color(0xFF465D91)
|
||||
val surfaceDimDark = Color(0xFF121318)
|
||||
val surfaceBrightDark = Color(0xFF38393E)
|
||||
val surfaceContainerLowestDark = Color(0xFF0C0E13)
|
||||
val surfaceContainerLowDark = Color(0xFF1A1B20)
|
||||
val surfaceContainerDark = Color(0xFF1E1F25)
|
||||
val surfaceContainerHighDark = Color(0xFF282A2F)
|
||||
val surfaceContainerHighestDark = Color(0xFF33353A)
|
||||
|
||||
val primaryDarkMediumContrast = Color(0xFFD0DCFF)
|
||||
val onPrimaryDarkMediumContrast = Color(0xFF042355)
|
||||
val primaryContainerDarkMediumContrast = Color(0xFF7990C7)
|
||||
val onPrimaryContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val secondaryDarkMediumContrast = Color(0xFFD5DCF3)
|
||||
val onSecondaryDarkMediumContrast = Color(0xFF1E2536)
|
||||
val secondaryContainerDarkMediumContrast = Color(0xFF8990A5)
|
||||
val onSecondaryContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val tertiaryDarkMediumContrast = Color(0xFFF6D1F4)
|
||||
val onTertiaryDarkMediumContrast = Color(0xFF351D37)
|
||||
val tertiaryContainerDarkMediumContrast = Color(0xFFA786A7)
|
||||
val onTertiaryContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val errorDarkMediumContrast = Color(0xFFFFD2CC)
|
||||
val onErrorDarkMediumContrast = Color(0xFF540003)
|
||||
val errorContainerDarkMediumContrast = Color(0xFFFF5449)
|
||||
val onErrorContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val backgroundDarkMediumContrast = Color(0xFF121318)
|
||||
val onBackgroundDarkMediumContrast = Color(0xFFE2E2E9)
|
||||
val surfaceDarkMediumContrast = Color(0xFF121318)
|
||||
val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF)
|
||||
val surfaceVariantDarkMediumContrast = Color(0xFF44464F)
|
||||
val onSurfaceVariantDarkMediumContrast = Color(0xFFDBDCE6)
|
||||
val outlineDarkMediumContrast = Color(0xFFB0B1BB)
|
||||
val outlineVariantDarkMediumContrast = Color(0xFF8E9099)
|
||||
val scrimDarkMediumContrast = Color(0xFF000000)
|
||||
val inverseSurfaceDarkMediumContrast = Color(0xFFE2E2E9)
|
||||
val inverseOnSurfaceDarkMediumContrast = Color(0xFF282A2F)
|
||||
val inversePrimaryDarkMediumContrast = Color(0xFF2E4779)
|
||||
val surfaceDimDarkMediumContrast = Color(0xFF121318)
|
||||
val surfaceBrightDarkMediumContrast = Color(0xFF43444A)
|
||||
val surfaceContainerLowestDarkMediumContrast = Color(0xFF06070C)
|
||||
val surfaceContainerLowDarkMediumContrast = Color(0xFF1C1D23)
|
||||
val surfaceContainerDarkMediumContrast = Color(0xFF26282D)
|
||||
val surfaceContainerHighDarkMediumContrast = Color(0xFF313238)
|
||||
val surfaceContainerHighestDarkMediumContrast = Color(0xFF3C3D43)
|
||||
|
||||
val primaryDarkHighContrast = Color(0xFFECEFFF)
|
||||
val onPrimaryDarkHighContrast = Color(0xFF000000)
|
||||
val primaryContainerDarkHighContrast = Color(0xFFAAC2FD)
|
||||
val onPrimaryContainerDarkHighContrast = Color(0xFF000A23)
|
||||
val secondaryDarkHighContrast = Color(0xFFECEFFF)
|
||||
val onSecondaryDarkHighContrast = Color(0xFF000000)
|
||||
val secondaryContainerDarkHighContrast = Color(0xFFBBC2D8)
|
||||
val onSecondaryContainerDarkHighContrast = Color(0xFF040B1B)
|
||||
val tertiaryDarkHighContrast = Color(0xFFFFEAFB)
|
||||
val onTertiaryDarkHighContrast = Color(0xFF000000)
|
||||
val tertiaryContainerDarkHighContrast = Color(0xFFDBB8DA)
|
||||
val onTertiaryContainerDarkHighContrast = Color(0xFF18031B)
|
||||
val errorDarkHighContrast = Color(0xFFFFECE9)
|
||||
val onErrorDarkHighContrast = Color(0xFF000000)
|
||||
val errorContainerDarkHighContrast = Color(0xFFFFAEA4)
|
||||
val onErrorContainerDarkHighContrast = Color(0xFF220001)
|
||||
val backgroundDarkHighContrast = Color(0xFF121318)
|
||||
val onBackgroundDarkHighContrast = Color(0xFFE2E2E9)
|
||||
val surfaceDarkHighContrast = Color(0xFF121318)
|
||||
val onSurfaceDarkHighContrast = Color(0xFFFFFFFF)
|
||||
val surfaceVariantDarkHighContrast = Color(0xFF44464F)
|
||||
val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF)
|
||||
val outlineDarkHighContrast = Color(0xFFEFEFF9)
|
||||
val outlineVariantDarkHighContrast = Color(0xFFC1C2CC)
|
||||
val scrimDarkHighContrast = Color(0xFF000000)
|
||||
val inverseSurfaceDarkHighContrast = Color(0xFFE2E2E9)
|
||||
val inverseOnSurfaceDarkHighContrast = Color(0xFF000000)
|
||||
val inversePrimaryDarkHighContrast = Color(0xFF2E4779)
|
||||
val surfaceDimDarkHighContrast = Color(0xFF121318)
|
||||
val surfaceBrightDarkHighContrast = Color(0xFF4F5056)
|
||||
val surfaceContainerLowestDarkHighContrast = Color(0xFF000000)
|
||||
val surfaceContainerLowDarkHighContrast = Color(0xFF1E1F25)
|
||||
val surfaceContainerDarkHighContrast = Color(0xFF2F3036)
|
||||
val surfaceContainerHighDarkHighContrast = Color(0xFF3A3B41)
|
||||
val surfaceContainerHighestDarkHighContrast = Color(0xFF45464C)
|
||||
@@ -0,0 +1,47 @@
|
||||
package ru.shadowsparky.ui.theme
|
||||
|
||||
import androidx.compose.material3.AppBarWithSearchColors
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.NavigationBarDefaults
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.material3.TopAppBarColors
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import ru.shadowsparky.ui.isBlurEnabled
|
||||
|
||||
object ComponentDefaults {
|
||||
|
||||
@Composable
|
||||
fun topAppBarColors(): TopAppBarColors {
|
||||
return if (isBlurEnabled()) {
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
Color.Transparent,
|
||||
Color.Transparent
|
||||
)
|
||||
} else {
|
||||
TopAppBarDefaults.topAppBarColors()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun navBarContentColor(): Color {
|
||||
return if (isBlurEnabled()) {
|
||||
Color.Transparent
|
||||
} else {
|
||||
NavigationBarDefaults.containerColor
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun appBarWithSearchDefaults(): AppBarWithSearchColors {
|
||||
return if (isBlurEnabled()) {
|
||||
SearchBarDefaults.appBarWithSearchColors(
|
||||
appBarContainerColor = Color.Transparent
|
||||
)
|
||||
} else {
|
||||
SearchBarDefaults.appBarWithSearchColors()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package ru.shadowsparky.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.MaterialExpressiveTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
val lightScheme = lightColorScheme(
|
||||
primary = primaryLight,
|
||||
onPrimary = onPrimaryLight,
|
||||
primaryContainer = primaryContainerLight,
|
||||
onPrimaryContainer = onPrimaryContainerLight,
|
||||
secondary = secondaryLight,
|
||||
onSecondary = onSecondaryLight,
|
||||
secondaryContainer = secondaryContainerLight,
|
||||
onSecondaryContainer = onSecondaryContainerLight,
|
||||
tertiary = tertiaryLight,
|
||||
onTertiary = onTertiaryLight,
|
||||
tertiaryContainer = tertiaryContainerLight,
|
||||
onTertiaryContainer = onTertiaryContainerLight,
|
||||
error = errorLight,
|
||||
onError = onErrorLight,
|
||||
errorContainer = errorContainerLight,
|
||||
onErrorContainer = onErrorContainerLight,
|
||||
background = backgroundLight,
|
||||
onBackground = onBackgroundLight,
|
||||
surface = surfaceLight,
|
||||
onSurface = onSurfaceLight,
|
||||
surfaceVariant = surfaceVariantLight,
|
||||
onSurfaceVariant = onSurfaceVariantLight,
|
||||
outline = outlineLight,
|
||||
outlineVariant = outlineVariantLight,
|
||||
scrim = scrimLight,
|
||||
inverseSurface = inverseSurfaceLight,
|
||||
inverseOnSurface = inverseOnSurfaceLight,
|
||||
inversePrimary = inversePrimaryLight,
|
||||
surfaceDim = surfaceDimLight,
|
||||
surfaceBright = surfaceBrightLight,
|
||||
surfaceContainerLowest = surfaceContainerLowestLight,
|
||||
surfaceContainerLow = surfaceContainerLowLight,
|
||||
surfaceContainer = surfaceContainerLight,
|
||||
surfaceContainerHigh = surfaceContainerHighLight,
|
||||
surfaceContainerHighest = surfaceContainerHighestLight,
|
||||
)
|
||||
|
||||
val darkScheme = darkColorScheme(
|
||||
primary = primaryDark,
|
||||
onPrimary = onPrimaryDark,
|
||||
primaryContainer = primaryContainerDark,
|
||||
onPrimaryContainer = onPrimaryContainerDark,
|
||||
secondary = secondaryDark,
|
||||
onSecondary = onSecondaryDark,
|
||||
secondaryContainer = secondaryContainerDark,
|
||||
onSecondaryContainer = onSecondaryContainerDark,
|
||||
tertiary = tertiaryDark,
|
||||
onTertiary = onTertiaryDark,
|
||||
tertiaryContainer = tertiaryContainerDark,
|
||||
onTertiaryContainer = onTertiaryContainerDark,
|
||||
error = errorDark,
|
||||
onError = onErrorDark,
|
||||
errorContainer = errorContainerDark,
|
||||
onErrorContainer = onErrorContainerDark,
|
||||
background = backgroundDark,
|
||||
onBackground = onBackgroundDark,
|
||||
surface = surfaceDark,
|
||||
onSurface = onSurfaceDark,
|
||||
surfaceVariant = surfaceVariantDark,
|
||||
onSurfaceVariant = onSurfaceVariantDark,
|
||||
outline = outlineDark,
|
||||
outlineVariant = outlineVariantDark,
|
||||
scrim = scrimDark,
|
||||
inverseSurface = inverseSurfaceDark,
|
||||
inverseOnSurface = inverseOnSurfaceDark,
|
||||
inversePrimary = inversePrimaryDark,
|
||||
surfaceDim = surfaceDimDark,
|
||||
surfaceBright = surfaceBrightDark,
|
||||
surfaceContainerLowest = surfaceContainerLowestDark,
|
||||
surfaceContainerLow = surfaceContainerLowDark,
|
||||
surfaceContainer = surfaceContainerDark,
|
||||
surfaceContainerHigh = surfaceContainerHighDark,
|
||||
surfaceContainerHighest = surfaceContainerHighestDark,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
MaterialExpressiveTheme(
|
||||
colorScheme = getColorScheme(),
|
||||
typography = AppTypography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getColorScheme(): ColorScheme {
|
||||
return when {
|
||||
isSystemInDarkTheme() -> darkDynamicScheme() ?: darkScheme
|
||||
else -> lightDynamicScheme() ?: lightScheme
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun lightDynamicScheme(): ColorScheme?
|
||||
|
||||
@Composable
|
||||
expect fun darkDynamicScheme(): ColorScheme?
|
||||
@@ -0,0 +1,5 @@
|
||||
package ru.shadowsparky.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
|
||||
val AppTypography = Typography()
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package ru.shadowsparky.ui.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
actual fun isTv(): Boolean = false
|
||||
@@ -0,0 +1,10 @@
|
||||
package ru.shadowsparky.ui.theme
|
||||
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
actual fun lightDynamicScheme(): ColorScheme? = null
|
||||
|
||||
@Composable
|
||||
actual fun darkDynamicScheme(): ColorScheme? = null
|
||||
@@ -0,0 +1,80 @@
|
||||
package ru.shadowsparky.ui
|
||||
|
||||
import com.arkivanov.essenty.lifecycle.Lifecycle
|
||||
import com.arkivanov.essenty.lifecycle.LifecycleRegistry
|
||||
import com.arkivanov.essenty.lifecycle.create
|
||||
import com.arkivanov.essenty.lifecycle.pause
|
||||
import com.arkivanov.essenty.lifecycle.resume
|
||||
import com.arkivanov.essenty.lifecycle.start
|
||||
import com.arkivanov.essenty.lifecycle.stop
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.events.Event
|
||||
|
||||
@JsFun(
|
||||
"""
|
||||
function hideElementById(id) {
|
||||
var el = document.getElementById(id);
|
||||
el.style.display = 'none';
|
||||
}
|
||||
"""
|
||||
)
|
||||
external fun hideElementById(id: String)
|
||||
|
||||
@JsFun("(function(){ return document.visibilityState; })")
|
||||
external fun pageVisibilityState(): String
|
||||
|
||||
fun hookPageVisibility(lifecycle: LifecycleRegistry) {
|
||||
|
||||
fun bootstrap() {
|
||||
lifecycle.resume()
|
||||
}
|
||||
|
||||
fun onVisibilityChange() {
|
||||
when (pageVisibilityState()) {
|
||||
"visible" -> lifecycle.resume()
|
||||
else -> lifecycle.stop()
|
||||
}
|
||||
}
|
||||
|
||||
fun onPageHideShow(hidden: Boolean) {
|
||||
if (hidden) {
|
||||
// уходим глубже: STARTED → CREATED
|
||||
when (lifecycle.state) {
|
||||
Lifecycle.State.RESUMED -> {
|
||||
lifecycle.pause(); lifecycle.stop()
|
||||
}
|
||||
|
||||
Lifecycle.State.STARTED -> {
|
||||
lifecycle.stop()
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
} else {
|
||||
// pageshow: поднимемся обратно
|
||||
when (lifecycle.state) {
|
||||
Lifecycle.State.INITIALIZED -> {
|
||||
lifecycle.create(); lifecycle.start(); if (pageVisibilityState() == "visible") lifecycle.resume()
|
||||
}
|
||||
|
||||
Lifecycle.State.CREATED -> {
|
||||
lifecycle.start(); if (pageVisibilityState() == "visible") lifecycle.resume()
|
||||
}
|
||||
|
||||
Lifecycle.State.STARTED -> {
|
||||
if (pageVisibilityState() == "visible") lifecycle.resume()
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bootstrap()
|
||||
|
||||
val visHandler: (Event) -> Unit = { onVisibilityChange() }
|
||||
document.addEventListener("visibilitychange", visHandler)
|
||||
|
||||
document.addEventListener("pagehide", { onPageHideShow(true) })
|
||||
document.addEventListener("pageshow", { onPageHideShow(false) })
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.shadowsparky.ui.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
actual fun isTv(): Boolean = false
|
||||
@@ -0,0 +1,10 @@
|
||||
package ru.shadowsparky.ui.theme
|
||||
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
actual fun lightDynamicScheme(): ColorScheme? = null
|
||||
|
||||
@Composable
|
||||
actual fun darkDynamicScheme(): ColorScheme? = null
|
||||
Reference in New Issue
Block a user