init projects

This commit is contained in:
2026-06-17 18:58:41 +03:00
parent 8543c4e241
commit 21a768edd5
505 changed files with 19998 additions and 93 deletions
@@ -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()