Initial commit

This commit is contained in:
tanexc
2026-06-12 23:49:11 +03:00
commit f69ffd79c2
222 changed files with 7094 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+66
View File
@@ -0,0 +1,66 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidMultiplatformLibrary)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
kotlin {
android {
namespace = "ru.tanexc.hemoanalysis.settings"
minSdk = 26
compileSdk {
version = release(36)
}
androidResources {
enable = true
}
}
listOf(
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "settingskit"
isStatic = true
}
}
sourceSets {
commonMain {
dependencies {
api(projects.domain)
api(projects.core.navigation)
api(projects.tool.analysis)
// kotlin
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.coroutines.core)
// compose
implementation(libs.compose.runtime)
implementation(libs.compose.foundation)
implementation(libs.compose.material3)
implementation(libs.compose.ui)
implementation(libs.compose.components.resources)
implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.androidx.graphics.shapes)
// decompose
implementation(libs.decompose)
implementation(libs.decompose.extensions.compose)
implementation(libs.decompose.extensions.compose.experimental)
implementation(libs.essenty.lifecycle)
// koin
implementation(libs.koin.core)
}
}
}
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m336,680 l144,-144 144,144 56,-56 -144,-144 144,-144 -56,-56 -144,144 -144,-144 -56,56 144,144 -144,144 56,56ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-134 -93,-227t-227,-93q-134,0 -227,93t-93,227q0,134 93,227t227,93ZM480,480Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M446,514ZM552,419ZM446,514ZM552,419ZM446,514ZM552,419ZM791,904 L56,169l56,-57 736,736 -57,56ZM480,840q-151,0 -255.5,-46.5T120,680v-400q0,-26 17.5,-49.5T187,187l252,252q-72,-3 -133,-18t-106,-40v120q51,29 123,44t157,15q20,0 39,-0.5t38,-2.5l70,70q-34,7 -71,10t-76,3q-85,0 -157,-15t-123,-44v99q9,29 97.5,54.5T480,760q64,0 128.5,-13T715,715l58,58q-49,31 -125.5,49T480,840ZM830,717 L760,647v-66q-11,6 -22,11t-23,10l-61,-61q30,-8 56.5,-17.5T760,501v-120q-41,23 -94,37t-116,19l-76,-76q44,0 92,-7t89.5,-18.5q41.5,-11.5 70,-26T760,281q-11,-29 -100.5,-55T480,200q-37,0 -75.5,5T331,218l-66,-66q45,-15 100,-23.5t115,-8.5q149,0 254.5,47T840,280v400q0,10 -2.5,19t-7.5,18Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M465,620.5q-7,-2.5 -13,-8.5L308,468q-12,-12 -11.5,-28t11.5,-28q12,-12 28.5,-12.5T365,411l75,75v-286q0,-17 11.5,-28.5T480,160q17,0 28.5,11.5T520,200v286l75,-75q12,-12 28.5,-11.5T652,412q11,12 11.5,28T652,468L508,612q-6,6 -13,8.5t-15,2.5q-8,0 -15,-2.5ZM240,800q-33,0 -56.5,-23.5T160,720v-80q0,-17 11.5,-28.5T200,600q17,0 28.5,11.5T240,640v80h480v-80q0,-17 11.5,-28.5T760,600q17,0 28.5,11.5T800,640v80q0,33 -23.5,56.5T720,800L240,800Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M393,828q-103,-29 -168,-113.5T160,520q0,-57 19,-108.5t54,-94.5q11,-12 27,-12.5t29,12.5q11,11 11.5,27T290,374q-24,31 -37,68t-13,78q0,81 47.5,144.5T410,751q13,4 21.5,15t8.5,24q0,20 -14,31.5t-33,6.5ZM567,828q-19,5 -33,-7t-14,-32q0,-12 8.5,-23t21.5,-15q75,-24 122.5,-87T720,520q0,-100 -70,-170t-170,-70h-3l16,16q11,11 11,28t-11,28q-11,11 -28,11t-28,-11l-84,-84q-6,-6 -8.5,-13t-2.5,-15q0,-8 2.5,-15t8.5,-13l84,-84q11,-11 28,-11t28,11q11,11 11,28t-11,28l-16,16h3q134,0 227,93t93,227q0,109 -65,194T567,828Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,15 @@
<resources>
<string name="settings">Настройки</string>
<string name="model_info_title">Текущая модель</string>
<string name="model_info_version">Версия:</string>
<string name="model_info_created_at">Дата создания:</string>
<string name="model_info_last_update">Последнее обновление:</string>
<string name="model_info_check_for_updates">Проверить обновление</string>
<string name="model_info_update_available">Доступно обновление!</string>
<string name="model_info_error">Ошибка загрузки. Попробуйте снова</string>
<string name="model_info_download_model">Загрузить модель</string>
<string name="model_info_no_model">Нет установленной модели</string>
<string name="model_info_size_title">Размер:</string>
<string name="model_info_size_content">Мб</string>
<string name="api_key_label">API-ключ</string>
</resources>
@@ -0,0 +1,15 @@
<resources>
<string name="settings">Settings</string>
<string name="model_info_title">CurrentModel</string>
<string name="model_info_version">Version:</string>
<string name="model_info_created_at">Created at:</string>
<string name="model_info_last_update">Last update at:</string>
<string name="model_info_check_for_updates">Check for updates</string>
<string name="model_info_update_available">An update is available!</string>
<string name="model_info_download_model">Download model</string>
<string name="model_info_error">Loading Error. Try Again</string>
<string name="model_info_no_model">Model is not installed</string>
<string name="model_info_size_title">Size:</string>
<string name="model_info_size_content">Mb</string>
<string name="api_key_label">API-key</string>
</resources>
@@ -0,0 +1,127 @@
package ru.tanexc.hemoanalysis.settings.di
import com.arkivanov.decompose.ComponentContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.tanexc.hemoanalysis.domain.State
import ru.tanexc.hemoanalysis.domain.repository.ModelRepository
import ru.tanexc.hemoanalysis.navigation.BaseComponent
import ru.tanexc.hemoanalysis.navigation.Config
import ru.tanexc.hemoanalysis.settings.presentation.LoadingState
import ru.tanexc.hemoanalysis.settings.presentation.ModelStatus
import ru.tanexc.hemoanalysis.settings.presentation.SettingsUiEvent
import ru.tanexc.hemoanalysis.settings.presentation.SettingsUiState
class SettingsComponent(
componentContext: ComponentContext,
onNavigate: (Config) -> Unit,
onBack: () -> Unit,
private val modelRepository: ModelRepository
) : BaseComponent(componentContext, onNavigate, onBack) {
private val _state = MutableStateFlow(
SettingsUiState(
currentModelInfo = modelRepository.getCurrentModelInfo(),
latestModelInfo = null,
loadingState = LoadingState.Idle,
apiKey = modelRepository.getKeyApi()
)
)
val state: StateFlow<SettingsUiState> = _state.asStateFlow()
init {
componentScope.launch(Dispatchers.IO) {
if (state.value.apiKey.isNotEmpty()) {
loadModelInfo()
}
}
}
fun onEvent(event: SettingsUiEvent) {
when (event) {
SettingsUiEvent.LoadLatestModel -> loadLatestModel()
SettingsUiEvent.UpdateModelInfo -> updateModelInfo()
SettingsUiEvent.NavigateBack -> onBack()
is SettingsUiEvent.UpdateApiKey -> updateApiKey(event.apiKey)
}
}
fun loadLatestModel() {
componentScope.launch(Dispatchers.IO) {
modelRepository.loadLatestModel().collect { state ->
when (state) {
is State.Success -> {
_state.update {
it.copy(
loadingState = LoadingState.Success(status = ModelStatus.UpToDate),
latestModelInfo = state.data,
currentModelInfo = state.data
)
}
}
is State.Error -> _state.update { it.copy(loadingState = LoadingState.Error(status = ModelStatus.OutOfDate)) }
is State.Loading -> _state.update { it.copy(loadingState = LoadingState.Loading) }
}
}
}
}
fun updateModelInfo() {
componentScope.launch(Dispatchers.IO) {
modelRepository.updateModelInfo().collect { state ->
when (state) {
is State.Success -> {
val latestModelInfo = state.data
val currentModelInfo = _state.value.currentModelInfo
val status = when {
currentModelInfo == null -> ModelStatus.NotPresented
!currentModelInfo.isLatest -> ModelStatus.OutOfDate
else -> ModelStatus.UpToDate
}
_state.update {
it.copy(
loadingState = LoadingState.Success(status = status),
latestModelInfo = latestModelInfo,
currentModelInfo = modelRepository.getCurrentModelInfo()
)
}
}
is State.Error -> _state.update { it.copy(loadingState = LoadingState.Error(status = ModelStatus.UpToDate)) }
is State.Loading -> _state.update { it.copy(loadingState = LoadingState.Loading) }
}
}
}
}
fun updateApiKey(apiKey: String) {
componentScope.launch(Dispatchers.IO) {
modelRepository.updateApiKey(apiKey)
_state.update { it.copy(apiKey = apiKey) }
if (apiKey.isNotEmpty()) {
loadModelInfo()
}
}
}
fun loadModelInfo() {
val currentModelInfo = modelRepository.getCurrentModelInfo()
if (currentModelInfo == null) {
_state.update { it.copy(loadingState = LoadingState.Success(status = ModelStatus.NotPresented)) }
} else {
_state.update {
it.copy(
currentModelInfo = currentModelInfo,
loadingState = LoadingState.Success(status = ModelStatus.UpToDate)
)
}
}
updateModelInfo()
}
}
@@ -0,0 +1,11 @@
package ru.tanexc.hemoanalysis.settings.di
import com.arkivanov.decompose.ComponentContext
import org.koin.dsl.module
import ru.tanexc.hemoanalysis.navigation.Config
val settingsModule = module {
factory<SettingsComponent> { (context: ComponentContext, onNavigate: (Config) -> Unit, onBack: () -> Unit) ->
SettingsComponent(context, onNavigate, onBack, get())
}
}
@@ -0,0 +1,17 @@
package ru.tanexc.hemoanalysis.settings.presentation
sealed interface LoadingState {
data object Idle: LoadingState
data class Success(
val status: ModelStatus
): LoadingState
data class Error(
val status: ModelStatus
): LoadingState
data object Loading: LoadingState
data object ApiKeyNotSet: LoadingState
}
@@ -0,0 +1,5 @@
package ru.tanexc.hemoanalysis.settings.presentation
enum class ModelStatus {
NotPresented, OutOfDate, UpToDate
}
@@ -0,0 +1,13 @@
package ru.tanexc.hemoanalysis.settings.presentation
sealed interface SettingsUiEvent {
data object UpdateModelInfo: SettingsUiEvent
data object LoadLatestModel: SettingsUiEvent
data object NavigateBack: SettingsUiEvent
data class UpdateApiKey(
val apiKey: String
): SettingsUiEvent
}
@@ -0,0 +1,10 @@
package ru.tanexc.hemoanalysis.settings.presentation
import ru.tanexc.hemoanalysis.domain.model.ModelInfo
data class SettingsUiState(
val currentModelInfo: ModelInfo?,
val latestModelInfo: ModelInfo?,
val loadingState: LoadingState,
val apiKey: String
)
@@ -0,0 +1,183 @@
package ru.tanexc.hemoanalysis.settings.ui
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import hemoanalysis.feature.settings.generated.resources.Res
import hemoanalysis.feature.settings.generated.resources.api_key_label
import hemoanalysis.feature.settings.generated.resources.cancel
import hemoanalysis.feature.settings.generated.resources.model_info_created_at
import hemoanalysis.feature.settings.generated.resources.model_info_last_update
import hemoanalysis.feature.settings.generated.resources.model_info_no_model
import hemoanalysis.feature.settings.generated.resources.model_info_title
import hemoanalysis.feature.settings.generated.resources.model_info_version
import hemoanalysis.feature.settings.generated.resources.settings
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import ru.tanexc.hemoanalysis.domain.model.ModelInfo
import ru.tanexc.hemoanalysis.settings.di.SettingsComponent
import ru.tanexc.hemoanalysis.settings.presentation.LoadingState
import ru.tanexc.hemoanalysis.settings.presentation.ModelStatus
import ru.tanexc.hemoanalysis.settings.presentation.SettingsUiEvent
import ru.tanexc.hemoanalysis.settings.ui.components.ErrorCard
import ru.tanexc.hemoanalysis.settings.ui.components.ModelInfoButton
import ru.tanexc.hemoanalysis.settings.ui.components.ModelInfoCard
import ru.tanexc.hemoanalysis.settings.ui.components.NoModelCard
import kotlin.time.Clock
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SettingsScreen(
paddingValues: PaddingValues,
component: SettingsComponent
) {
val state by component.state.collectAsState()
val focusManager = LocalFocusManager.current
Column(
modifier = Modifier.padding(
top = 0.dp,
bottom = paddingValues.calculateBottomPadding()
).fillMaxSize()
) {
CenterAlignedTopAppBar(
title = { Text(stringResource(Res.string.settings)) }
)
Column(
modifier = Modifier
.fillMaxSize()
.hideKeyboardOnTap(focusManager)
.padding(horizontal = 12.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Spacer(Modifier.size(8.dp))
when (val modelInfo = state.currentModelInfo) {
null -> NoModelCard()
else -> ModelInfoCard(
version = modelInfo.version,
size = modelInfo.size,
createdAtTime = modelInfo.createdAt,
lastUpdateTime = modelInfo.lastUpdateTime
)
}
TextField(
modifier = Modifier.fillMaxWidth(),
value = state.apiKey,
onValueChange = { component.onEvent(SettingsUiEvent.UpdateApiKey(it)) },
label = {
Text(
text = stringResource(Res.string.api_key_label),
fontWeight = FontWeight.Bold
)
},
singleLine = true,
maxLines = 1,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
}
),
colors = TextFieldDefaults.colors(
unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer,
focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer,
errorContainerColor = MaterialTheme.colorScheme.secondaryContainer,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent
),
shape = RoundedCornerShape(36.dp),
trailingIcon = {
if (state.apiKey.isNotEmpty()) {
IconButton(
onClick = { component.onEvent(SettingsUiEvent.UpdateApiKey("")) }
) {
Icon(
painter = painterResource(Res.drawable.cancel),
contentDescription = null
)
}
}
},
isError = state.apiKey.isEmpty()
)
when (val loadingState = state.loadingState) {
is LoadingState.Success -> ModelInfoButton(
status = loadingState.status,
isEnabled = true,
onClick = {
when (loadingState.status) {
ModelStatus.UpToDate -> component.onEvent(SettingsUiEvent.UpdateModelInfo)
else -> component.onEvent(SettingsUiEvent.LoadLatestModel)
}
}
)
is LoadingState.Error -> ErrorCard(onClick = {
when (loadingState.status) {
ModelStatus.UpToDate -> component.onEvent(SettingsUiEvent.UpdateModelInfo)
else -> component.onEvent(SettingsUiEvent.LoadLatestModel)
}
})
is LoadingState.Loading -> LinearProgressIndicator(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
)
is LoadingState.ApiKeyNotSet,
is LoadingState.Idle -> Unit
}
}
}
}
private fun Modifier.hideKeyboardOnTap(focusManager: FocusManager): Modifier =
pointerInput(focusManager) {
detectTapGestures {
focusManager.clearFocus()
}
}
@@ -0,0 +1,56 @@
package ru.tanexc.hemoanalysis.settings.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearWavyProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import hemoanalysis.feature.settings.generated.resources.Res
import hemoanalysis.feature.settings.generated.resources.model_info_error
import hemoanalysis.feature.settings.generated.resources.restart
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@Composable
fun ErrorCard(
onClick: () -> Unit
) {
Button(
modifier = Modifier.fillMaxWidth(),
shape = CircleShape,
onClick = onClick,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Box(
modifier = Modifier.fillMaxWidth().padding(8.dp),
) {
Icon(
painter = painterResource(Res.drawable.restart),
contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.align(Alignment.CenterStart)
)
Text(
text = stringResource(Res.string.model_info_error),
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.align(Alignment.Center)
)
}
}
}
@@ -0,0 +1,79 @@
package ru.tanexc.hemoanalysis.settings.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearWavyProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import hemoanalysis.feature.settings.generated.resources.Res
import hemoanalysis.feature.settings.generated.resources.download
import hemoanalysis.feature.settings.generated.resources.model_info_check_for_updates
import hemoanalysis.feature.settings.generated.resources.model_info_download_model
import hemoanalysis.feature.settings.generated.resources.model_info_error
import hemoanalysis.feature.settings.generated.resources.model_info_update_available
import hemoanalysis.feature.settings.generated.resources.restart
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import ru.tanexc.hemoanalysis.settings.presentation.ModelStatus
@Composable
fun ModelInfoButton(
status: ModelStatus,
isEnabled: Boolean,
onClick: () -> Unit
) {
Button(
modifier = Modifier.fillMaxWidth(),
shape = CircleShape,
enabled = isEnabled,
onClick = onClick,
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary),
) {
val (text, icon) = when (status) {
ModelStatus.NotPresented -> stringResource(Res.string.model_info_download_model) to painterResource(
Res.drawable.download
)
ModelStatus.OutOfDate -> stringResource(Res.string.model_info_update_available) to painterResource(
Res.drawable.download
)
ModelStatus.UpToDate -> stringResource(Res.string.model_info_check_for_updates) to painterResource(
Res.drawable.restart
)
}
Box(
modifier = Modifier.fillMaxWidth().padding(8.dp),
) {
Icon(
painter = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.align(Alignment.CenterStart)
)
Text(
text = text,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.align(Alignment.Center)
)
}
}
}
@@ -0,0 +1,112 @@
package ru.tanexc.hemoanalysis.settings.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import hemoanalysis.feature.settings.generated.resources.Res
import hemoanalysis.feature.settings.generated.resources.model_info_created_at
import hemoanalysis.feature.settings.generated.resources.model_info_last_update
import hemoanalysis.feature.settings.generated.resources.model_info_size_content
import hemoanalysis.feature.settings.generated.resources.model_info_size_title
import hemoanalysis.feature.settings.generated.resources.model_info_title
import hemoanalysis.feature.settings.generated.resources.model_info_version
import org.jetbrains.compose.resources.stringResource
import ru.tanexc.hemoanalysis.domain.model.Mb
import ru.tanexc.hemoanalysis.domain.model.Size
import kotlin.math.round
@Composable
fun ModelInfoCard(
version: String,
size: Size,
createdAtTime: String,
lastUpdateTime: String,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(36.dp)),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Column(
modifier = Modifier
.padding(22.dp)
.fillMaxWidth(),
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(Res.string.model_info_title),
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
textAlign = TextAlign.Center
)
Spacer(Modifier.size(22.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(Res.string.model_info_version),
fontWeight = FontWeight.Bold
)
Text(text = version)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(Res.string.model_info_size_title),
fontWeight = FontWeight.Bold
)
Text(text = "${size.formattedMb()} " + stringResource(Res.string.model_info_size_content))
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(Res.string.model_info_created_at),
fontWeight = FontWeight.Bold
)
Text(text = createdAtTime)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(Res.string.model_info_last_update),
fontWeight = FontWeight.Bold
)
Text(text = lastUpdateTime)
}
}
}
}
private fun Size.formattedMb(): String {
val rounded = round(Mb * 10) / 10
return rounded.toString()
}
@@ -0,0 +1,57 @@
package ru.tanexc.hemoanalysis.settings.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import hemoanalysis.feature.settings.generated.resources.Res
import hemoanalysis.feature.settings.generated.resources.database_off
import hemoanalysis.feature.settings.generated.resources.model_info_no_model
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@Composable
fun NoModelCard() {
Card(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(36.dp)),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
) {
Column(
modifier = Modifier
.padding(22.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
modifier = Modifier.size(56.dp),
painter = painterResource(Res.drawable.database_off),
tint = MaterialTheme.colorScheme.onSecondaryContainer,
contentDescription = null,
)
Text(
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
text = stringResource(Res.string.model_info_no_model),
color = MaterialTheme.colorScheme.onSecondaryContainer,
)
}
}
}