Initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,72 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidMultiplatformLibrary)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
android {
|
||||
namespace = "ru.tanexc.hemoanalysis.analysis"
|
||||
|
||||
minSdk = 26
|
||||
|
||||
compileSdk {
|
||||
version = release(36)
|
||||
}
|
||||
|
||||
androidResources {
|
||||
enable = true
|
||||
}
|
||||
}
|
||||
|
||||
listOf(
|
||||
iosArm64(),
|
||||
iosSimulatorArm64()
|
||||
).forEach { iosTarget ->
|
||||
iosTarget.binaries.framework {
|
||||
baseName = "analysiskit"
|
||||
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)
|
||||
|
||||
// file kit
|
||||
implementation(libs.filekit.core)
|
||||
implementation(libs.filekit.dialogs)
|
||||
implementation(libs.filekit.dialogs.compose)
|
||||
implementation(libs.filekit.coil)
|
||||
|
||||
// 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>
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.ImageBuffer
|
||||
|
||||
internal actual val ImageBuffer.imageBitmap: ImageBitmap
|
||||
get() = Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888).asImageBitmap()
|
||||
@@ -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="M400,760v-80q-83,0 -141.5,-58.5T200,480q0,-61 33.5,-111t90.5,-73q8,-34 35.5,-55t62.5,-21l-9,-25q-5,-16 1.5,-30.5T437,144h1q-6,-15 1,-29.5t24,-20.5q15,-5 29.5,1.5T512,118q16,-6 31,1t21,23l82,225q6,16 -0.5,30.5T623,418h-1q6,16 -1,31t-24,21q-15,5 -29.5,-1.5T548,446q-16,6 -31,-1t-21,-23l-10,-28q-15,14 -34.5,21t-39.5,5q-22,-2 -41,-13.5T338,378q-27,16 -42.5,43T280,480q0,50 35,85t85,35h280q17,0 28.5,11.5T720,640q0,17 -11.5,28.5T680,680L520,680v80h200q17,0 28.5,11.5T760,800q0,17 -11.5,28.5T720,840L240,840q-17,0 -28.5,-11.5T200,800q0,-17 11.5,-28.5T240,760h160ZM546,382 L582,368 514,180 476,194 546,382ZM448.5,348.5Q460,337 460,320t-11.5,-28.5Q437,280 420,280t-28.5,11.5Q380,303 380,320t11.5,28.5Q403,360 420,360t28.5,-11.5ZM546,382ZM420,320ZM420,320Z"
|
||||
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="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,44 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="450dp"
|
||||
android:height="450dp"
|
||||
android:viewportWidth="64"
|
||||
android:viewportHeight="64">
|
||||
<path
|
||||
android:pathData="M56.25,18V46L32,60 7.75,46V18L32,4Z"
|
||||
android:fillColor="#6075f2"/>
|
||||
<path
|
||||
android:pathData="m41.5,26.5v11L32,43V60L56.25,46V18Z"
|
||||
android:fillColor="#6b57ff"/>
|
||||
<path
|
||||
android:pathData="m32,43 l-9.5,-5.5v-11L7.75,18V46L32,60Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:centerX="23.131"
|
||||
android:centerY="18.441"
|
||||
android:gradientRadius="42.132"
|
||||
android:type="radial">
|
||||
<item android:offset="0" android:color="#FF5383EC"/>
|
||||
<item android:offset="0.867" android:color="#FF7F52FF"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M22.5,26.5 L32,21 41.5,26.5 56.25,18 32,4 7.75,18Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="44.172"
|
||||
android:startY="4.377"
|
||||
android:endX="17.973"
|
||||
android:endY="34.035"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF33C3FF"/>
|
||||
<item android:offset="0.878" android:color="#FF5383EC"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="m32,21 l9.526,5.5v11L32,43 22.474,37.5v-11z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:pathData="M6.713,27.346C4.706,25.6 3.388,23.387 2.757,20.71C2.125,18.032 2.32,15.472 3.339,13.031C4.358,10.589 6.137,9.077 8.676,8.494C11.215,7.911 13.488,8.493 15.495,10.24C17.501,11.986 18.82,14.199 19.451,16.876C20.082,19.554 19.888,22.114 18.868,24.555C17.849,26.997 16.07,28.509 13.531,29.092C10.992,29.675 8.719,29.093 6.713,27.346ZM16.142,22.902C16.698,21.218 16.758,19.449 16.321,17.595C15.884,15.741 15.039,14.18 13.787,12.912C12.535,11.643 11.074,11.2 9.405,11.584C7.735,11.967 6.622,13.001 6.066,14.685C5.51,16.368 5.45,18.137 5.887,19.991C6.324,21.845 7.168,23.406 8.42,24.674C9.673,25.943 11.133,26.386 12.803,26.002C14.472,25.619 15.585,24.585 16.142,22.902ZM13.543,22.141C13.791,21.795 13.871,21.396 13.783,20.946C13.696,20.496 13.476,20.148 13.125,19.903C12.903,19.737 12.7,19.539 12.518,19.31C12.335,19.08 12.212,18.828 12.147,18.553C12.082,18.279 12.08,17.999 12.142,17.713C12.203,17.428 12.296,17.162 12.421,16.916C12.627,16.544 12.679,16.142 12.578,15.713C12.476,15.284 12.233,14.951 11.847,14.714C11.469,14.511 11.072,14.458 10.655,14.554C10.237,14.65 9.908,14.888 9.668,15.269C9.294,15.861 9.052,16.505 8.941,17.2C8.83,17.895 8.855,18.586 9.017,19.272C9.179,19.959 9.456,20.592 9.849,21.171C10.243,21.75 10.739,22.224 11.339,22.593C11.725,22.83 12.127,22.901 12.544,22.805C12.962,22.709 13.294,22.488 13.543,22.141Z"
|
||||
android:fillColor="#E3E3E3"/>
|
||||
<path
|
||||
android:pathData="M25.8,8.887C27.188,6.63 29.162,4.958 31.721,3.873C34.28,2.788 36.867,2.525 39.481,3.085C42.096,3.644 43.918,5.106 44.947,7.47C45.976,9.834 45.796,12.145 44.408,14.403C43.02,16.66 41.047,18.331 38.488,19.417C35.929,20.502 33.342,20.765 30.727,20.205C28.113,19.645 26.291,18.183 25.262,15.819C24.233,13.455 24.412,11.145 25.8,8.887ZM31.896,17.264C33.673,17.508 35.447,17.254 37.219,16.502C38.991,15.751 40.398,14.655 41.443,13.215C42.487,11.774 42.671,10.277 41.994,8.723C41.317,7.168 40.09,6.269 38.313,6.026C36.536,5.782 34.761,6.036 32.99,6.787C31.218,7.539 29.81,8.635 28.766,10.075C27.722,11.515 27.538,13.013 28.215,14.567C28.891,16.121 30.118,17.02 31.896,17.264ZM32.195,14.605C32.585,14.785 32.996,14.793 33.43,14.628C33.863,14.463 34.171,14.189 34.354,13.804C34.48,13.559 34.642,13.328 34.838,13.11C35.035,12.892 35.265,12.728 35.527,12.616C35.79,12.505 36.069,12.454 36.364,12.463C36.659,12.472 36.941,12.516 37.208,12.594C37.616,12.728 38.025,12.708 38.435,12.534C38.846,12.36 39.135,12.064 39.303,11.648C39.439,11.245 39.422,10.849 39.253,10.461C39.083,10.072 38.788,9.795 38.366,9.629C37.709,9.37 37.024,9.248 36.312,9.263C35.599,9.277 34.915,9.424 34.258,9.702C33.602,9.98 33.02,10.361 32.512,10.845C32.004,11.33 31.618,11.896 31.357,12.544C31.188,12.961 31.188,13.363 31.358,13.752C31.527,14.14 31.806,14.425 32.195,14.605Z"
|
||||
android:fillColor="#E3E3E3"/>
|
||||
<path
|
||||
android:pathData="M30.19,43.863C32.421,42.406 34.036,40.395 35.035,37.828C36.035,35.262 36.2,32.701 35.531,30.144C34.863,27.587 33.312,25.847 30.879,24.924C28.445,24.001 26.113,24.268 23.882,25.725C21.65,27.181 20.035,29.193 19.036,31.759C18.037,34.325 17.872,36.887 18.54,39.444C19.208,42.001 20.759,43.741 23.193,44.664C25.626,45.587 27.958,45.32 30.19,43.863ZM21.472,38.177C21.156,36.434 21.344,34.674 22.036,32.897C22.728,31.121 23.782,29.689 25.2,28.603C26.617,27.517 28.126,27.278 29.726,27.885C31.326,28.492 32.284,29.667 32.599,31.41C32.915,33.154 32.727,34.914 32.035,36.69C31.344,38.467 30.289,39.898 28.872,40.984C27.454,42.07 25.945,42.31 24.346,41.703C22.746,41.096 21.788,39.921 21.472,38.177ZM24.152,37.778C23.955,37.401 23.931,36.995 24.081,36.561C24.231,36.127 24.497,35.813 24.878,35.618C25.122,35.484 25.35,35.315 25.563,35.113C25.776,34.91 25.933,34.678 26.036,34.414C26.138,34.151 26.179,33.874 26.159,33.583C26.138,33.292 26.083,33.016 25.993,32.756C25.842,32.358 25.846,31.954 26.006,31.543C26.166,31.132 26.454,30.835 26.869,30.653C27.272,30.503 27.673,30.504 28.073,30.656C28.473,30.808 28.765,31.089 28.95,31.499C29.237,32.136 29.387,32.807 29.4,33.51C29.413,34.214 29.292,34.894 29.035,35.552C28.779,36.21 28.416,36.799 27.946,37.32C27.476,37.84 26.917,38.242 26.271,38.525C25.856,38.708 25.448,38.723 25.048,38.571C24.649,38.419 24.35,38.155 24.152,37.778Z"
|
||||
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="M451.5,828.5Q440,817 440,800v-160q0,-17 11.5,-28.5T480,600q17,0 28.5,11.5T520,640v40h280q17,0 28.5,11.5T840,720q0,17 -11.5,28.5T800,760L520,760v40q0,17 -11.5,28.5T480,840q-17,0 -28.5,-11.5ZM160,760q-17,0 -28.5,-11.5T120,720q0,-17 11.5,-28.5T160,680h160q17,0 28.5,11.5T360,720q0,17 -11.5,28.5T320,760L160,760ZM291.5,588.5Q280,577 280,560v-40L160,520q-17,0 -28.5,-11.5T120,480q0,-17 11.5,-28.5T160,440h120v-40q0,-17 11.5,-28.5T320,360q17,0 28.5,11.5T360,400v160q0,17 -11.5,28.5T320,600q-17,0 -28.5,-11.5ZM480,520q-17,0 -28.5,-11.5T440,480q0,-17 11.5,-28.5T480,440h320q17,0 28.5,11.5T840,480q0,17 -11.5,28.5T800,520L480,520ZM611.5,348.5Q600,337 600,320v-160q0,-17 11.5,-28.5T640,120q17,0 28.5,11.5T680,160v40h120q17,0 28.5,11.5T840,240q0,17 -11.5,28.5T800,280L680,280v40q0,17 -11.5,28.5T640,360q-17,0 -28.5,-11.5ZM160,280q-17,0 -28.5,-11.5T120,240q0,-17 11.5,-28.5T160,200h320q17,0 28.5,11.5T520,240q0,17 -11.5,28.5T480,280L160,280Z"
|
||||
android:fillColor="#e3e3e3"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,53 @@
|
||||
<resources>
|
||||
<string name="grant_permissions">Предоставьте разрешения!</string>
|
||||
<string name="grant_permissions_text">Разрешения необдимы для работы анализа</string>
|
||||
<string name="grant_text">Предоставить</string>
|
||||
|
||||
<string name="select_image">Выбрать изображение</string>
|
||||
<string name="start_analysis">Начать анализ</string>
|
||||
|
||||
<string name="confidence">Уверенность</string>
|
||||
<string name="iou">Допустимое пересечение</string>
|
||||
|
||||
<string name="analysis">Анализ</string>
|
||||
|
||||
<string name="cell_class_lymphocyte">Лимфоциты</string>
|
||||
<string name="cell_class_metamyelocyte">Метамиелоциты</string>
|
||||
<string name="cell_class_monocyte">Моноциты</string>
|
||||
<string name="cell_class_myelocyte">Миелоциты</string>
|
||||
<string name="cell_class_normoblasts">Нормобласты</string>
|
||||
<string name="cell_class_rod_neutrophil">Палочкоядерные нейтрофилы</string>
|
||||
<string name="cell_class_segmentonuclear_neutrophil">Сегментоядерные нейтрофилы</string>
|
||||
<string name="cell_class_blast">Бласты</string>
|
||||
<string name="cell_class_promyelocyte">Промиелоциты</string>
|
||||
<string name="cell_class_eosinophil">Эозинофилы</string>
|
||||
<string name="cell_class_basophil">Базофилы</string>
|
||||
<string name="cell_class_magakariocyte">Мегакариоциты</string>
|
||||
<string name="cell_class_bad_cells">Плохие клетки</string>
|
||||
<string name="cell_class_plasma_cell">Плазматические клетки</string>
|
||||
<string name="cell_class_artifacts">Артефакты</string>
|
||||
<string name="cell_class_gumpricht_shadows">Тени Гумпрехта</string>
|
||||
<string name="cell_class_methoses">Митозы</string>
|
||||
<string name="cell_class_normoblasts_oxyfilny">Нормобласты оксифильные</string>
|
||||
<string name="cell_class_macrophages">Макрофаги</string>
|
||||
<string name="cell_class_normoblasts_erythroblasts">Нормобласты эритробластические</string>
|
||||
<string name="cell_class_normoblasts_basophilic">Нормобласты базофильные</string>
|
||||
<string name="cell_class_normoblasts_polychromatophilic">Нормобласты полихроматофильные</string>
|
||||
|
||||
<string name="report_title">Отчёт</string>
|
||||
<string name="report_inference_duration">Время инференса:</string>
|
||||
<string name="report_preprocessing_duration">Время предобработки:</string>
|
||||
<string name="report_postprocessing_duration">Время постобработки:</string>
|
||||
<string name="report_total_duration">Общее время:</string>
|
||||
|
||||
<string name="parameters_title">Параметры анализа</string>
|
||||
<string name="preprocess_variant_deafult">По умолчанию</string>
|
||||
<string name="preprocess_variant_brightness_contrast">Яркость и контраст</string>
|
||||
|
||||
<string name="roi_variant_none">Отключено</string>
|
||||
<string name="roi_variant_threshold_filter">Пороговая фильтрация</string>
|
||||
<string name="roi_variant_morph">Пороговая маска</string>
|
||||
|
||||
<string name="roi_params_title">Выделение областей интереса</string>
|
||||
<string name="preprocess_params_title">Предобработка</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,53 @@
|
||||
<resources>
|
||||
<string name="grant_permissions">Grant permissions!</string>
|
||||
<string name="grant_permissions_text">Grant permissions to access image analysis</string>
|
||||
<string name="grant_text">Grant</string>
|
||||
|
||||
<string name="select_image">Select an image</string>
|
||||
<string name="start_analysis">Start analysis</string>
|
||||
|
||||
<string name="confidence">Confidence</string>
|
||||
<string name="iou">Intersection over Union</string>
|
||||
|
||||
<string name="analysis">Analysis</string>
|
||||
|
||||
<string name="cell_class_lymphocyte">Lymphocytes</string>
|
||||
<string name="cell_class_metamyelocyte">Metamyelocytes</string>
|
||||
<string name="cell_class_monocyte">Monocytes</string>
|
||||
<string name="cell_class_myelocyte">Myelocytes</string>
|
||||
<string name="cell_class_normoblasts">Normoblastes</string>
|
||||
<string name="cell_class_rod_neutrophil">Rod neutrophiles</string>
|
||||
<string name="cell_class_segmentonuclear_neutrophil">Segmentonuclear neutrophils</string>
|
||||
<string name="cell_class_blast">Blasts</string>
|
||||
<string name="cell_class_promyelocyte">Promyelocytes</string>
|
||||
<string name="cell_class_eosinophil">Eosinophils</string>
|
||||
<string name="cell_class_basophil">Basophils</string>
|
||||
<string name="cell_class_magakariocyte">Megakaryocytes</string>
|
||||
<string name="cell_class_bad_cells">Bad cells</string>
|
||||
<string name="cell_class_plasma_cell">Plasma cells</string>
|
||||
<string name="cell_class_artifacts">Artifacts</string>
|
||||
<string name="cell_class_gumpricht_shadows">Gumpricht shadows</string>
|
||||
<string name="cell_class_methoses">Mitoses</string>
|
||||
<string name="cell_class_normoblasts_oxyfilny">Oxyphilic normoblasts</string>
|
||||
<string name="cell_class_macrophages">Macrophages</string>
|
||||
<string name="cell_class_normoblasts_erythroblasts">Erythroblast normoblasts</string>
|
||||
<string name="cell_class_normoblasts_basophilic">Basophilic normoblasts</string>
|
||||
<string name="cell_class_normoblasts_polychromatophilic">Polychromatophilic normoblasts</string>
|
||||
|
||||
<string name="report_title">Report</string>
|
||||
<string name="report_inference_duration">Inference duration:</string>
|
||||
<string name="report_preprocessing_duration">Preprocessing duration:</string>
|
||||
<string name="report_postprocessing_duration">Postprocessing duration:</string>
|
||||
<string name="report_total_duration">Total duration:</string>
|
||||
|
||||
<string name="parameters_title">Analysis parameters</string>
|
||||
<string name="preprocess_variant_deafult">Default</string>
|
||||
<string name="preprocess_variant_brightness_contrast">Brightness contrast</string>
|
||||
|
||||
<string name="roi_variant_none">None</string>
|
||||
<string name="roi_variant_threshold_filter">Threshold filter</string>
|
||||
<string name="roi_variant_morph">Threshold morph</string>
|
||||
|
||||
<string name="roi_params_title">Highlight regions of interests</string>
|
||||
<string name="preprocess_params_title">Preprocessing</string>
|
||||
</resources>
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.di
|
||||
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import io.github.vinceglb.filekit.FileKit
|
||||
import io.github.vinceglb.filekit.dialogs.FileKitType
|
||||
import io.github.vinceglb.filekit.dialogs.compose.util.toImageBitmap
|
||||
import io.github.vinceglb.filekit.dialogs.openFilePicker
|
||||
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.analysis.presentation.AnalysisUiEvent
|
||||
import ru.tanexc.hemoanalysis.analysis.presentation.AnalysisUiState
|
||||
import ru.tanexc.hemoanalysis.analysis.presentation.Status
|
||||
import ru.tanexc.hemoanalysis.analysis.presentation.UpdateParametersUiEvent
|
||||
import ru.tanexc.hemoanalysis.analysis.util.imageBuffer
|
||||
import ru.tanexc.hemoanalysis.navigation.BaseComponent
|
||||
import ru.tanexc.hemoanalysis.navigation.Config
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters.AnalysisParameters
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.results.AnalysisResult
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.AnalysisTool
|
||||
import ru.tanexc.hemoanalysis.domain.repository.AnalysisParamsRepository
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters.PreprocessVariant
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters.RoiVariant
|
||||
|
||||
class AnalysisComponent(
|
||||
componentContext: ComponentContext,
|
||||
onNavigate: (Config) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
private val analysisTool: AnalysisTool,
|
||||
private val analysisParamsRepositoryImpl: AnalysisParamsRepository
|
||||
) : BaseComponent(componentContext, onNavigate, onBack) {
|
||||
private val _state = MutableStateFlow(
|
||||
AnalysisUiState(
|
||||
confidenceThreshold = analysisParamsRepositoryImpl.confThreshold,
|
||||
iouThreshold = analysisParamsRepositoryImpl.iouThreshold,
|
||||
preprocessVariant = PreprocessVariant.entries[analysisParamsRepositoryImpl.preprocessVariant],
|
||||
roiVariant = RoiVariant.entries[analysisParamsRepositoryImpl.roiVariant]
|
||||
)
|
||||
)
|
||||
val state: StateFlow<AnalysisUiState> = _state.asStateFlow()
|
||||
|
||||
fun onEvent(event: AnalysisUiEvent) {
|
||||
when (event) {
|
||||
is AnalysisUiEvent.SelectImage -> selectImage()
|
||||
is AnalysisUiEvent.StartAnalysis -> startAnalysis()
|
||||
is AnalysisUiEvent.CancelImage -> cancelImage()
|
||||
is UpdateParametersUiEvent.ChangeConfidenceThreshold -> {
|
||||
_state.update { it.copy(confidenceThreshold = event.value) }
|
||||
analysisParamsRepositoryImpl.confThreshold = event.value
|
||||
}
|
||||
|
||||
is UpdateParametersUiEvent.ChangeIouThreshold -> {
|
||||
_state.update { it.copy(iouThreshold = event.value) }
|
||||
analysisParamsRepositoryImpl.iouThreshold = event.value
|
||||
}
|
||||
|
||||
is AnalysisUiEvent.CloseParameters -> _state.update { it.copy(showParameters = false) }
|
||||
is AnalysisUiEvent.OpenParameters -> _state.update { it.copy(showParameters = true) }
|
||||
is UpdateParametersUiEvent.ChangePreprocessVariant -> _state.update {
|
||||
analysisParamsRepositoryImpl.preprocessVariant = event.value.ordinal
|
||||
it.copy(preprocessVariant = event.value)
|
||||
}
|
||||
|
||||
is UpdateParametersUiEvent.ChangeRoiVariant -> _state.update {
|
||||
analysisParamsRepositoryImpl.roiVariant = event.value.ordinal
|
||||
it.copy(roiVariant = event.value)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectImage() {
|
||||
componentScope.launch {
|
||||
FileKit.openFilePicker(FileKitType.Image)?.let { imageFile ->
|
||||
_state.update {
|
||||
it.copy(imageBitmap = imageFile.toImageBitmap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAnalysis() {
|
||||
componentScope.launch {
|
||||
_state.update { it.copy(status = Status.Loading) }
|
||||
val bitmap = state.value.imageBitmap ?: return@launch
|
||||
val params = AnalysisParameters(
|
||||
imageBuffer = bitmap.imageBuffer,
|
||||
confThreshold = state.value.confidenceThreshold,
|
||||
iouThreshold = state.value.iouThreshold,
|
||||
preprocessVariant = state.value.preprocessVariant,
|
||||
roiVariant = state.value.roiVariant
|
||||
)
|
||||
val result = analysisTool.performAnalysis(params)
|
||||
|
||||
when (result) {
|
||||
is AnalysisResult.Error -> _state.update {
|
||||
it.copy(status = Status.Failed(exception = result.exception))
|
||||
}
|
||||
|
||||
is AnalysisResult.Success -> _state.update {
|
||||
it.copy(
|
||||
status = Status.Success(
|
||||
detections = result.detection,
|
||||
duration = result.duration
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelImage() {
|
||||
_state.update {
|
||||
it.copy(
|
||||
status = Status.Idle,
|
||||
imageBitmap = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.di
|
||||
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import org.koin.dsl.module
|
||||
import ru.tanexc.hemoanalysis.navigation.Config
|
||||
|
||||
val analysisModule = module {
|
||||
factory<AnalysisComponent> { (context: ComponentContext, onNavigate: (Config) -> Unit, onBack: () -> Unit) ->
|
||||
AnalysisComponent(context, onNavigate, onBack, get(), get())
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.presentation
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters.PreprocessVariant
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters.RoiVariant
|
||||
|
||||
sealed interface AnalysisUiEvent {
|
||||
data object SelectImage: AnalysisUiEvent
|
||||
|
||||
data object StartAnalysis: AnalysisUiEvent
|
||||
|
||||
data object CancelImage: AnalysisUiEvent
|
||||
|
||||
data object OpenParameters: AnalysisUiEvent
|
||||
|
||||
data object CloseParameters: AnalysisUiEvent
|
||||
}
|
||||
|
||||
sealed interface UpdateParametersUiEvent: AnalysisUiEvent {
|
||||
data class ChangeConfidenceThreshold(
|
||||
val value: Float
|
||||
): UpdateParametersUiEvent
|
||||
|
||||
data class ChangeIouThreshold(
|
||||
val value: Float
|
||||
): UpdateParametersUiEvent
|
||||
|
||||
data class ChangePreprocessVariant(
|
||||
val value: PreprocessVariant
|
||||
): UpdateParametersUiEvent
|
||||
|
||||
data class ChangeRoiVariant(
|
||||
val value: RoiVariant
|
||||
): UpdateParametersUiEvent
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.presentation
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters.PreprocessVariant
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters.RoiVariant
|
||||
|
||||
data class AnalysisUiState(
|
||||
val status: Status = Status.Idle,
|
||||
val imageBitmap: ImageBitmap? = null,
|
||||
val showParameters: Boolean = false,
|
||||
val confidenceThreshold: Float = 0.5f,
|
||||
val iouThreshold: Float = 0.5f,
|
||||
val preprocessVariant: PreprocessVariant = PreprocessVariant.Default,
|
||||
val roiVariant: RoiVariant = RoiVariant.None
|
||||
)
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.presentation
|
||||
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.AnalysisException
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.results.AnalysisDuration
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.results.Detection
|
||||
|
||||
sealed interface Status {
|
||||
data object Idle: Status
|
||||
|
||||
data object Loading: Status
|
||||
|
||||
data class Success(
|
||||
val detections: List<Detection>,
|
||||
val duration: AnalysisDuration
|
||||
): Status
|
||||
|
||||
data class Failed(
|
||||
val exception: AnalysisException
|
||||
): Status
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
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.unit.dp
|
||||
import hemoanalysis.feature.analysis.generated.resources.Res
|
||||
import hemoanalysis.feature.analysis.generated.resources.analysis
|
||||
import hemoanalysis.feature.analysis.generated.resources.parameters
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import ru.tanexc.hemoanalysis.analysis.di.AnalysisComponent
|
||||
import ru.tanexc.hemoanalysis.analysis.presentation.AnalysisUiEvent
|
||||
import ru.tanexc.hemoanalysis.analysis.presentation.Status
|
||||
import ru.tanexc.hemoanalysis.analysis.presentation.UpdateParametersUiEvent
|
||||
import ru.tanexc.hemoanalysis.analysis.ui.components.AnalysisControlButton
|
||||
import ru.tanexc.hemoanalysis.analysis.ui.components.AnalysisParametersSheet
|
||||
import ru.tanexc.hemoanalysis.analysis.ui.components.Picture
|
||||
import ru.tanexc.hemoanalysis.analysis.ui.components.errorBlock
|
||||
import ru.tanexc.hemoanalysis.analysis.ui.components.reportBlock
|
||||
import ru.tanexc.hemoanalysis.analysis.util.round
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun AnalysisScreen(
|
||||
paddingValues: PaddingValues,
|
||||
component: AnalysisComponent
|
||||
) {
|
||||
val state by component
|
||||
.state
|
||||
.collectAsState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
top = 0.dp,
|
||||
bottom = paddingValues.calculateBottomPadding()
|
||||
).fillMaxSize()
|
||||
) {
|
||||
CenterAlignedTopAppBar(
|
||||
title = { Text(stringResource(Res.string.analysis)) },
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { component.onEvent(AnalysisUiEvent.OpenParameters) },
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.parameters),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer
|
||||
)
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.weight(1f),
|
||||
overscrollEffect = null,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(0.dp, 0.dp, 36.dp, 36.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Picture(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
imageBitmap = state.imageBitmap,
|
||||
status = state.status,
|
||||
onSelectImage = { component.onEvent(AnalysisUiEvent.SelectImage) }
|
||||
)
|
||||
Spacer(Modifier.size(12.dp))
|
||||
AnalysisControlButton(
|
||||
onSelectImage = { component.onEvent(AnalysisUiEvent.SelectImage) },
|
||||
onCancelImage = { component.onEvent(AnalysisUiEvent.CancelImage) },
|
||||
onStartAnalysis = { component.onEvent(AnalysisUiEvent.StartAnalysis) },
|
||||
isImageSelected = state.imageBitmap != null,
|
||||
isAnalysisInProgress = state.status == Status.Loading
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
when (val status = state.status) {
|
||||
is Status.Success -> reportBlock(
|
||||
detections = status.detections,
|
||||
duration = status.duration
|
||||
)
|
||||
|
||||
is Status.Failed -> errorBlock(
|
||||
status.exception
|
||||
)
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AnalysisParametersSheet(
|
||||
isVisible = state.showParameters,
|
||||
confidenceThreshold = state.confidenceThreshold,
|
||||
iouThreshold = state.iouThreshold,
|
||||
onDismiss = { component.onEvent(AnalysisUiEvent.CloseParameters) },
|
||||
onConfThresholdChange = {
|
||||
component.onEvent(
|
||||
UpdateParametersUiEvent.ChangeConfidenceThreshold(
|
||||
it.round(2)
|
||||
)
|
||||
)
|
||||
},
|
||||
onIouThresholdChange = {
|
||||
component.onEvent(
|
||||
UpdateParametersUiEvent.ChangeIouThreshold(
|
||||
it.round(2)
|
||||
)
|
||||
)
|
||||
},
|
||||
preprocessVariant = state.preprocessVariant,
|
||||
roiVariant = state.roiVariant,
|
||||
onPreprocessVariantChange = {
|
||||
component.onEvent(
|
||||
UpdateParametersUiEvent.ChangePreprocessVariant(it)
|
||||
)
|
||||
},
|
||||
onRoiVariantChange = {
|
||||
component.onEvent(
|
||||
UpdateParametersUiEvent.ChangeRoiVariant(it)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import hemoanalysis.feature.analysis.generated.resources.Res
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_artifacts
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_bad_cells
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_basophil
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_blast
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_eosinophil
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_gumpricht_shadows
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_lymphocyte
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_macrophages
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_magakariocyte
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_metamyelocyte
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_methoses
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_monocyte
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_myelocyte
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_normoblasts
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_normoblasts_basophilic
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_normoblasts_erythroblasts
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_normoblasts_oxyfilny
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_normoblasts_polychromatophilic
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_plasma_cell
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_promyelocyte
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_rod_neutrophil
|
||||
import hemoanalysis.feature.analysis.generated.resources.cell_class_segmentonuclear_neutrophil
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.results.CellClass
|
||||
|
||||
|
||||
fun CellClass.strokeColor(): Color = when (this) {
|
||||
CellClass.Blast -> Color(0xFFD50000)
|
||||
CellClass.BadCells -> Color(0xFFFF6D00)
|
||||
CellClass.Promyelocyte -> Color(0xFFC62828)
|
||||
CellClass.Myelocyte -> Color(0xFFAD1457)
|
||||
CellClass.Metamyelocyte -> Color(0xFF6A1B9A)
|
||||
|
||||
CellClass.RodNeutrophil -> Color(0xFF1565C0)
|
||||
CellClass.SegmentonuclearNeutrophil -> Color(0xFF00838F)
|
||||
|
||||
CellClass.Lymphocyte -> Color(0xFF2E7D32)
|
||||
CellClass.Monocyte -> Color(0xFF00695C)
|
||||
CellClass.Eosinophil -> Color(0xFFF9A825)
|
||||
CellClass.Basophil -> Color(0xFF283593)
|
||||
CellClass.Macrophages -> Color(0xFF00897B)
|
||||
CellClass.PlasmaCell -> Color(0xFF8E24AA)
|
||||
|
||||
CellClass.Magakariocyte -> Color(0xFF5D4037)
|
||||
CellClass.Methoses -> Color(0xFF4E342E)
|
||||
|
||||
CellClass.Normoblasts -> Color(0xFF3949AB)
|
||||
CellClass.NormoblastsOxyfilny -> Color(0xFFFF8A65)
|
||||
CellClass.NormoblastsErythroblasts -> Color(0xFFE53935)
|
||||
CellClass.NormoblastsBasophilic -> Color(0xFF1E88E5)
|
||||
CellClass.NormoblastsPolychromatophilic -> Color(0xFF8E24AA)
|
||||
|
||||
CellClass.Artifacts -> Color(0xFF757575)
|
||||
CellClass.GumprichtShadows -> Color(0xFF9E9E9E)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CellClass.title() = when (this) {
|
||||
CellClass.Lymphocyte -> stringResource(Res.string.cell_class_lymphocyte)
|
||||
CellClass.Metamyelocyte -> stringResource(Res.string.cell_class_metamyelocyte)
|
||||
CellClass.Monocyte -> stringResource(Res.string.cell_class_monocyte)
|
||||
CellClass.Myelocyte -> stringResource(Res.string.cell_class_myelocyte)
|
||||
CellClass.Normoblasts -> stringResource(Res.string.cell_class_normoblasts)
|
||||
CellClass.RodNeutrophil -> stringResource(Res.string.cell_class_rod_neutrophil)
|
||||
CellClass.SegmentonuclearNeutrophil -> stringResource(Res.string.cell_class_segmentonuclear_neutrophil)
|
||||
CellClass.Blast -> stringResource(Res.string.cell_class_blast)
|
||||
CellClass.Promyelocyte -> stringResource(Res.string.cell_class_promyelocyte)
|
||||
CellClass.Eosinophil -> stringResource(Res.string.cell_class_eosinophil)
|
||||
CellClass.Basophil -> stringResource(Res.string.cell_class_basophil)
|
||||
CellClass.Magakariocyte -> stringResource(Res.string.cell_class_magakariocyte)
|
||||
CellClass.BadCells -> stringResource(Res.string.cell_class_bad_cells)
|
||||
CellClass.PlasmaCell -> stringResource(Res.string.cell_class_plasma_cell)
|
||||
CellClass.Artifacts -> stringResource(Res.string.cell_class_artifacts)
|
||||
CellClass.GumprichtShadows -> stringResource(Res.string.cell_class_gumpricht_shadows)
|
||||
CellClass.Methoses -> stringResource(Res.string.cell_class_methoses)
|
||||
CellClass.NormoblastsOxyfilny -> stringResource(Res.string.cell_class_normoblasts_oxyfilny)
|
||||
CellClass.Macrophages -> stringResource(Res.string.cell_class_macrophages)
|
||||
CellClass.NormoblastsErythroblasts -> stringResource(Res.string.cell_class_normoblasts_erythroblasts)
|
||||
CellClass.NormoblastsBasophilic -> stringResource(Res.string.cell_class_normoblasts_basophilic)
|
||||
CellClass.NormoblastsPolychromatophilic -> stringResource(Res.string.cell_class_normoblasts_polychromatophilic)
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandHorizontally
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hemoanalysis.feature.analysis.generated.resources.Res
|
||||
import hemoanalysis.feature.analysis.generated.resources.cancel
|
||||
import hemoanalysis.feature.analysis.generated.resources.select_image
|
||||
import hemoanalysis.feature.analysis.generated.resources.start_analysis
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@Composable
|
||||
fun AnalysisControlButton(
|
||||
modifier: Modifier = Modifier,
|
||||
onSelectImage: () -> Unit,
|
||||
onCancelImage: () -> Unit,
|
||||
onStartAnalysis: () -> Unit,
|
||||
isImageSelected: Boolean,
|
||||
isAnalysisInProgress: Boolean
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.height(IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = if (isImageSelected) {
|
||||
RoundedCornerShape(50, 5, 5, 50)
|
||||
} else {
|
||||
CircleShape
|
||||
},
|
||||
enabled = !isAnalysisInProgress,
|
||||
onClick = {
|
||||
if (!isImageSelected) {
|
||||
onSelectImage()
|
||||
} else {
|
||||
onStartAnalysis()
|
||||
}
|
||||
}
|
||||
) {
|
||||
val text = if (!isImageSelected) {
|
||||
stringResource(Res.string.select_image)
|
||||
} else {
|
||||
stringResource(Res.string.start_analysis)
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
text = text
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
visible = isImageSelected,
|
||||
enter = slideInHorizontally() + expandHorizontally()
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
shape = RoundedCornerShape(5, 50, 50, 5),
|
||||
enabled = !isAnalysisInProgress,
|
||||
onClick = onCancelImage
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.cancel),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.ui.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.ButtonGroup
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import hemoanalysis.feature.analysis.generated.resources.Res
|
||||
import hemoanalysis.feature.analysis.generated.resources.confidence
|
||||
import hemoanalysis.feature.analysis.generated.resources.iou
|
||||
import hemoanalysis.feature.analysis.generated.resources.parameters_title
|
||||
import hemoanalysis.feature.analysis.generated.resources.preprocess_params_title
|
||||
import hemoanalysis.feature.analysis.generated.resources.preprocess_variant_brightness_contrast
|
||||
import hemoanalysis.feature.analysis.generated.resources.preprocess_variant_deafult
|
||||
import hemoanalysis.feature.analysis.generated.resources.roi_params_title
|
||||
import hemoanalysis.feature.analysis.generated.resources.roi_variant_morph
|
||||
import hemoanalysis.feature.analysis.generated.resources.roi_variant_none
|
||||
import hemoanalysis.feature.analysis.generated.resources.roi_variant_threshold_filter
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters.PreprocessVariant
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.parameters.RoiVariant
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun AnalysisParametersSheet(
|
||||
isVisible: Boolean,
|
||||
confidenceThreshold: Float,
|
||||
iouThreshold: Float,
|
||||
preprocessVariant: PreprocessVariant,
|
||||
roiVariant: RoiVariant,
|
||||
onDismiss: () -> Unit,
|
||||
onConfThresholdChange: (Float) -> Unit,
|
||||
onIouThresholdChange: (Float) -> Unit,
|
||||
onPreprocessVariantChange: (PreprocessVariant) -> Unit,
|
||||
onRoiVariantChange: (RoiVariant) -> Unit
|
||||
|
||||
) {
|
||||
if (isVisible) {
|
||||
ModalBottomSheet(
|
||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(Res.string.parameters_title),
|
||||
fontWeight = FontWeight.Black,
|
||||
fontSize = 24.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(22.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(22.dp)
|
||||
) {
|
||||
TitledSlider(
|
||||
value = confidenceThreshold,
|
||||
title = stringResource(Res.string.confidence),
|
||||
onValueChange = onConfThresholdChange
|
||||
)
|
||||
|
||||
TitledSlider(
|
||||
value = iouThreshold,
|
||||
title = stringResource(Res.string.iou),
|
||||
onValueChange = onIouThresholdChange
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.preprocess_params_title),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min)
|
||||
) {
|
||||
SegmentedButton(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
selected = preprocessVariant == PreprocessVariant.Default,
|
||||
onClick = { onPreprocessVariantChange(PreprocessVariant.Default) },
|
||||
shape = SegmentedButtonDefaults.itemShape(0, 2),
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(Res.string.preprocess_variant_deafult),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
},
|
||||
icon = {}
|
||||
)
|
||||
SegmentedButton(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
selected = preprocessVariant == PreprocessVariant.BrightnessContrast,
|
||||
onClick = { onPreprocessVariantChange(PreprocessVariant.BrightnessContrast) },
|
||||
shape = SegmentedButtonDefaults.itemShape(1, 2),
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(Res.string.preprocess_variant_brightness_contrast),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
},
|
||||
icon = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.roi_params_title),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min)
|
||||
) {
|
||||
SegmentedButton(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
selected = roiVariant == RoiVariant.None,
|
||||
onClick = { onRoiVariantChange(RoiVariant.None) },
|
||||
shape = SegmentedButtonDefaults.itemShape(0, 3),
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(Res.string.roi_variant_none),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
},
|
||||
icon = {}
|
||||
)
|
||||
SegmentedButton(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
selected = roiVariant == RoiVariant.ThresholdMorph,
|
||||
onClick = { onRoiVariantChange(RoiVariant.ThresholdMorph) },
|
||||
shape = SegmentedButtonDefaults.itemShape(1, 3),
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(Res.string.roi_variant_morph),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
},
|
||||
icon = {}
|
||||
)
|
||||
SegmentedButton(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
selected = roiVariant == RoiVariant.ThresholdFilter,
|
||||
onClick = { onRoiVariantChange(RoiVariant.ThresholdFilter) },
|
||||
shape = SegmentedButtonDefaults.itemShape(2, 3),
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(Res.string.roi_variant_threshold_filter),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
},
|
||||
icon = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TitledSlider(
|
||||
value: Float,
|
||||
title: String,
|
||||
onValueChange: (Float) -> Unit
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(value.toString())
|
||||
}
|
||||
|
||||
Slider(
|
||||
value = value,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
steps = 100,
|
||||
valueRange = 0f..1f,
|
||||
colors = SliderDefaults.colors().copy(
|
||||
activeTickColor = Color.Transparent,
|
||||
inactiveTickColor = Color.Transparent
|
||||
),
|
||||
onValueChange = onValueChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.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.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ru.tanexc.hemoanalysis.analysis.ui.strokeColor
|
||||
import ru.tanexc.hemoanalysis.analysis.ui.title
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.results.CellClass
|
||||
|
||||
@Composable
|
||||
fun CellHeader(
|
||||
cell: CellClass,
|
||||
count: Int
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.clip(RoundedCornerShape(36.dp))
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer)
|
||||
.padding(vertical = 16.dp, horizontal = 22.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(cell.strokeColor())
|
||||
.align(Alignment.CenterStart)
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.basicMarquee()
|
||||
.align(Alignment.Center),
|
||||
text = cell.title(),
|
||||
textAlign = TextAlign.Center,
|
||||
overflow = TextOverflow.Visible,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd),
|
||||
text = count.toString())
|
||||
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.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.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
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.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 androidx.compose.ui.unit.sp
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.AnalysisException
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
fun LazyListScope.errorBlock(
|
||||
analysisException: AnalysisException
|
||||
) {
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||
.clip(RoundedCornerShape(36.dp))
|
||||
.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(22.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Ошибка во время анализа",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Black,
|
||||
fontSize = 24.sp
|
||||
)
|
||||
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = "Время выполнения:",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = analysisException.duration.toString(
|
||||
unit = DurationUnit.MILLISECONDS,
|
||||
decimals = 1
|
||||
)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Text(
|
||||
text = "Ошибка:",
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
text = analysisException.message ?: "Неизвестна",
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
textAlign = TextAlign.Justify
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.ui.components
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.CircularWavyProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.toShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.graphics.shapes.CornerRounding
|
||||
import androidx.graphics.shapes.RoundedPolygon
|
||||
import androidx.graphics.shapes.star
|
||||
import hemoanalysis.feature.analysis.generated.resources.Res
|
||||
import hemoanalysis.feature.analysis.generated.resources.analysis
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import ru.tanexc.hemoanalysis.analysis.presentation.Status
|
||||
import ru.tanexc.hemoanalysis.analysis.ui.strokeColor
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun Picture(
|
||||
modifier: Modifier = Modifier,
|
||||
status: Status,
|
||||
imageBitmap: ImageBitmap? = null,
|
||||
onSelectImage: () -> Unit
|
||||
) {
|
||||
val isLoading = status == Status.Loading
|
||||
val detections by remember(status) {
|
||||
derivedStateOf {
|
||||
when (status) {
|
||||
is Status.Success -> status.detections
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imageBitmap?.let {
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(36.dp))
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(imageBitmap.width.toFloat() / imageBitmap.height)
|
||||
) {
|
||||
val height = size.height
|
||||
val width = size.width
|
||||
val scaleX = width / imageBitmap.width
|
||||
val scaleY = height / imageBitmap.height
|
||||
|
||||
drawImage(
|
||||
image = imageBitmap,
|
||||
srcSize = IntSize(imageBitmap.width, imageBitmap.height),
|
||||
dstSize = IntSize(width.toInt(), height.toInt()),
|
||||
alpha = if (isLoading) 0.3f else 1f
|
||||
)
|
||||
|
||||
detections.forEach { detection ->
|
||||
val left = (detection.centerX - detection.width / 2f) * scaleX
|
||||
val top = (detection.centerY - detection.height / 2f) * scaleY
|
||||
val boxWidth = detection.width * scaleX
|
||||
val boxHeight = detection.height * scaleY
|
||||
|
||||
drawRoundRect(
|
||||
color = detection.cellClass.strokeColor(),
|
||||
topLeft = Offset(left, top),
|
||||
size = Size(boxWidth, boxHeight),
|
||||
cornerRadius = CornerRadius(12f, 12f),
|
||||
style = Stroke(
|
||||
width = 8f,
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(15f, 10f), 0f)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
CircularWavyProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
} ?: Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(2f)
|
||||
.clip(RoundedCornerShape(36.dp))
|
||||
.clickable(onClick = onSelectImage),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val shape = RoundedPolygon.star(
|
||||
numVerticesPerRadius = 4,
|
||||
innerRadius = 0.4f,
|
||||
rounding = CornerRounding(0.5f, 0.2f)
|
||||
).normalized().toShape(45)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(140.dp)
|
||||
.clip(shape)
|
||||
.background(MaterialTheme.colorScheme.secondary, shape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.fillMaxSize(0.5f),
|
||||
painter = painterResource(Res.drawable.analysis),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.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.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Text
|
||||
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 androidx.compose.ui.unit.sp
|
||||
import hemoanalysis.feature.analysis.generated.resources.Res
|
||||
import hemoanalysis.feature.analysis.generated.resources.report_inference_duration
|
||||
import hemoanalysis.feature.analysis.generated.resources.report_postprocessing_duration
|
||||
import hemoanalysis.feature.analysis.generated.resources.report_preprocessing_duration
|
||||
import hemoanalysis.feature.analysis.generated.resources.report_title
|
||||
import hemoanalysis.feature.analysis.generated.resources.report_total_duration
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import ru.tanexc.hemoanalysis.analysis.ui.title
|
||||
import ru.tanexc.hemoanalysis.analysis.util.round
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.results.AnalysisDuration
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.results.Detection
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
fun LazyListScope.reportBlock(
|
||||
detections: List<Detection>,
|
||||
duration: AnalysisDuration
|
||||
) {
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||
.clip(RoundedCornerShape(36.dp))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(22.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(Res.string.report_preprocessing_duration),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = duration.preprocessing.toString(
|
||||
unit = DurationUnit.MILLISECONDS,
|
||||
decimals = 1
|
||||
)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(Res.string.report_inference_duration),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = duration.inference.toString(
|
||||
unit = DurationUnit.MILLISECONDS,
|
||||
decimals = 1
|
||||
)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(Res.string.report_postprocessing_duration),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = duration.postprocessing.toString(
|
||||
unit = DurationUnit.MILLISECONDS,
|
||||
decimals = 1
|
||||
)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(Res.string.report_total_duration),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = duration.total.toString(
|
||||
unit = DurationUnit.MILLISECONDS,
|
||||
decimals = 1
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (detections.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
modifier = Modifier,
|
||||
text = stringResource(Res.string.report_title),
|
||||
fontWeight = FontWeight.Black,
|
||||
fontSize = 24.sp
|
||||
)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
detections
|
||||
.groupBy { it.cellClass }
|
||||
.forEach { (cell, detects) ->
|
||||
stickyHeader {
|
||||
CellHeader(cell, detections.size)
|
||||
}
|
||||
items(detects) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp, horizontal = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(it.cellClass.title())
|
||||
Text(it.confidence.round(2).toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.util
|
||||
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.round
|
||||
|
||||
fun Float.round(n: Int): Float {
|
||||
return round(this * 10f.pow(n)) / 10f.pow(n)
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.util
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.ImageBuffer
|
||||
|
||||
internal expect val ImageBuffer.imageBitmap: ImageBitmap
|
||||
|
||||
internal val ImageBitmap.imageBuffer: ImageBuffer
|
||||
get() = ImageBuffer(
|
||||
pixels = IntArray(width * height).apply(::readPixels),
|
||||
width = width,
|
||||
height = height
|
||||
)
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package ru.tanexc.hemoanalysis.analysis.util
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||
import org.jetbrains.skia.ColorAlphaType
|
||||
import org.jetbrains.skia.ColorType
|
||||
import org.jetbrains.skia.Image
|
||||
import org.jetbrains.skia.ImageInfo
|
||||
import ru.tanexc.hemoanalysis.tool.analysis.api.domain.ImageBuffer
|
||||
|
||||
internal actual val ImageBuffer.imageBitmap: ImageBitmap
|
||||
get() {
|
||||
val bytes = ByteArray(width * height * 4)
|
||||
|
||||
var byteIndex = 0
|
||||
|
||||
for (pixel in pixels) {
|
||||
val a = pixel ushr 24 and 0xFF
|
||||
val r = pixel ushr 16 and 0xFF
|
||||
val g = pixel ushr 8 and 0xFF
|
||||
val b = pixel and 0xFF
|
||||
|
||||
bytes[byteIndex++] = b.toByte()
|
||||
bytes[byteIndex++] = g.toByte()
|
||||
bytes[byteIndex++] = r.toByte()
|
||||
bytes[byteIndex++] = a.toByte()
|
||||
}
|
||||
|
||||
val imageInfo = ImageInfo(
|
||||
width = width,
|
||||
height = height,
|
||||
colorType = ColorType.BGRA_8888,
|
||||
alphaType = ColorAlphaType.UNPREMUL
|
||||
)
|
||||
|
||||
return Image.makeRaster(
|
||||
imageInfo = imageInfo,
|
||||
bytes = bytes,
|
||||
rowBytes = width * 4
|
||||
).toComposeImageBitmap()
|
||||
}
|
||||
@@ -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