Initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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>
|
||||
+127
@@ -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()
|
||||
}
|
||||
}
|
||||
+11
@@ -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())
|
||||
}
|
||||
}
|
||||
+17
@@ -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
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package ru.tanexc.hemoanalysis.settings.presentation
|
||||
|
||||
enum class ModelStatus {
|
||||
NotPresented, OutOfDate, UpToDate
|
||||
}
|
||||
+13
@@ -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
|
||||
}
|
||||
+10
@@ -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
|
||||
)
|
||||
+183
@@ -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()
|
||||
}
|
||||
}
|
||||
+56
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+112
@@ -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()
|
||||
}
|
||||
+57
@@ -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,
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user