Initial commit
@@ -0,0 +1,21 @@
|
||||
*.iml
|
||||
.kotlin
|
||||
.gradle
|
||||
**/build/
|
||||
xcuserdata
|
||||
!src/**/build/
|
||||
local.properties
|
||||
.idea
|
||||
.DS_Store
|
||||
captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
!*.xcodeproj/xcshareddata/
|
||||
!*.xcodeproj/project.xcworkspace/
|
||||
!*.xcworkspace/contents.xcworkspacedata
|
||||
**/xcshareddata/WorkspaceSettings.xcsettings
|
||||
node_modules/
|
||||
Pods/
|
||||
**/Pods/
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,62 @@
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "ru.tanexc.hemoanalysis"
|
||||
compileSdk {
|
||||
version = release(36)
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "ru.tanexc.hemoanalysis"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
include("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
|
||||
isUniversalApk = false
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.shared)
|
||||
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(libs.filekit.core)
|
||||
implementation(libs.filekit.dialogs.compose)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
-keepattributes RuntimeVisibleAnnotations,RuntimeInvisibleAnnotations,Signature,InnerClasses,EnclosingMethod
|
||||
|
||||
-keep class **$$serializer { *; }
|
||||
-keepclassmembers class ** {
|
||||
public static ** serializer(...);
|
||||
}
|
||||
-keepclassmembers class **$Companion {
|
||||
public final kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
-keep @kotlinx.serialization.Serializable class ru.tanexc.hemoanalysis.** { *; }
|
||||
|
||||
-keep class io.ktor.** { *; }
|
||||
-keep class kotlinx.coroutines.** { *; }
|
||||
-keep class kotlinx.serialization.** { *; }
|
||||
-keepnames class io.ktor.** { *; }
|
||||
|
||||
-keep class ai.onnxruntime.** { *; }
|
||||
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
-dontwarn javax.annotation.**
|
||||
-dontwarn org.conscrypt.**
|
||||
-dontwarn org.bouncycastle.**
|
||||
-dontwarn org.openjsse.**
|
||||
-dontwarn java.lang.management.**
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "ru.tanexc.hemoanalysis",
|
||||
"variantName": "release",
|
||||
"elements": [
|
||||
{
|
||||
"type": "ONE_OF_MANY",
|
||||
"filters": [
|
||||
{
|
||||
"filterType": "ABI",
|
||||
"value": "armeabi-v7a"
|
||||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"versionCode": 1,
|
||||
"versionName": "1.0",
|
||||
"outputFile": "androidApp-armeabi-v7a-release.apk"
|
||||
},
|
||||
{
|
||||
"type": "ONE_OF_MANY",
|
||||
"filters": [
|
||||
{
|
||||
"filterType": "ABI",
|
||||
"value": "x86_64"
|
||||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"versionCode": 1,
|
||||
"versionName": "1.0",
|
||||
"outputFile": "androidApp-x86_64-release.apk"
|
||||
},
|
||||
{
|
||||
"type": "ONE_OF_MANY",
|
||||
"filters": [
|
||||
{
|
||||
"filterType": "ABI",
|
||||
"value": "arm64-v8a"
|
||||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"versionCode": 1,
|
||||
"versionName": "1.0",
|
||||
"outputFile": "androidApp-arm64-v8a-release.apk"
|
||||
},
|
||||
{
|
||||
"type": "ONE_OF_MANY",
|
||||
"filters": [
|
||||
{
|
||||
"filterType": "ABI",
|
||||
"value": "x86"
|
||||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"versionCode": 1,
|
||||
"versionName": "1.0",
|
||||
"outputFile": "androidApp-x86-release.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File",
|
||||
"baselineProfiles": [
|
||||
{
|
||||
"minApi": 28,
|
||||
"maxApi": 30,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/1/androidApp-armeabi-v7a-release.dm",
|
||||
"baselineProfiles/1/androidApp-x86_64-release.dm",
|
||||
"baselineProfiles/1/androidApp-arm64-v8a-release.dm",
|
||||
"baselineProfiles/1/androidApp-x86-release.dm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"minApi": 31,
|
||||
"maxApi": 2147483647,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/0/androidApp-armeabi-v7a-release.dm",
|
||||
"baselineProfiles/0/androidApp-x86_64-release.dm",
|
||||
"baselineProfiles/0/androidApp-arm64-v8a-release.dm",
|
||||
"baselineProfiles/0/androidApp-x86-release.dm"
|
||||
]
|
||||
}
|
||||
],
|
||||
"minSdkVersionForDexing": 26
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"
|
||||
android:minSdkVersion="34" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<application
|
||||
android:name=".HemoAnalysisApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_policy"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.HemoAnalysis">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.HemoAnalysis">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,14 @@
|
||||
package ru.tanexc.hemoanalysis
|
||||
|
||||
import android.app.Application
|
||||
import ru.tanexc.hemoanalysis.di.koinInit
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
|
||||
class HemoAnalysisApplication: Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
koinInit {
|
||||
androidContext(this@HemoAnalysisApplication)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package ru.tanexc.hemoanalysis
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import io.github.vinceglb.filekit.FileKit
|
||||
import io.github.vinceglb.filekit.dialogs.init
|
||||
import ru.tanexc.hemoanalysis.navigation.provideRootComponent
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
val root = provideRootComponent()
|
||||
FileKit.init(this)
|
||||
setContent {
|
||||
App(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="80"
|
||||
android:viewportHeight="80">
|
||||
<group android:scaleX="0.5"
|
||||
android:scaleY="0.5"
|
||||
android:translateX="20"
|
||||
android:translateY="20">
|
||||
<path
|
||||
android:pathData="M37.691,23.516C42.2,23.22 46.21,24.387 49.722,27.017C53.235,29.648 55.483,33.168 56.467,37.578C57.451,41.988 56.465,46.167 53.508,50.116C50.55,54.065 46.817,56.188 42.308,56.484C37.8,56.78 33.789,55.613 30.277,52.983C26.765,50.352 24.517,46.832 23.532,42.422C22.548,38.012 23.534,33.833 26.492,29.884C29.449,25.935 33.182,23.812 37.691,23.516ZM28.951,41.098C29.834,43.954 31.492,46.293 33.923,48.114C36.354,49.935 39.065,50.867 42.054,50.911C45.044,50.954 47.511,49.678 49.455,47.081C51.4,44.485 51.931,41.758 51.048,38.902C50.165,36.046 48.508,33.707 46.076,31.886C43.645,30.065 40.934,29.133 37.945,29.089C34.956,29.046 32.488,30.322 30.544,32.919C28.599,35.515 28.069,38.242 28.951,41.098ZM33.314,38.792C33.309,39.549 33.59,40.16 34.155,40.625C34.72,41.091 35.367,41.259 36.096,41.13C36.569,41.062 37.045,41.059 37.525,41.123C38.004,41.186 38.424,41.353 38.784,41.623C39.145,41.893 39.423,42.249 39.618,42.691C39.814,43.133 39.946,43.591 40.014,44.063C40.095,44.8 40.417,45.379 40.98,45.8C41.543,46.222 42.209,46.341 42.979,46.157C43.704,45.94 44.309,45.507 44.795,44.858C45.281,44.209 45.504,43.489 45.464,42.699C45.4,41.469 45.104,40.34 44.575,39.31C44.046,38.281 43.331,37.429 42.43,36.754C41.53,36.08 40.521,35.62 39.405,35.375C38.288,35.13 37.131,35.15 35.933,35.435C35.164,35.618 34.536,36.035 34.049,36.684C33.563,37.333 33.318,38.036 33.314,38.792Z"
|
||||
android:fillColor="#BCF0B4"/>
|
||||
<path
|
||||
android:pathData="M17.012,69.981C15.179,69.981 13.61,69.329 12.304,68.023C10.998,66.718 10.346,65.148 10.346,63.315V53.315C10.346,52.37 10.665,51.579 11.304,50.94C11.943,50.301 12.735,49.981 13.679,49.981C14.623,49.981 15.415,50.301 16.054,50.94C16.693,51.579 17.012,52.37 17.012,53.315V63.315H27.012C27.957,63.315 28.749,63.634 29.387,64.273C30.026,64.912 30.346,65.704 30.346,66.648C30.346,67.593 30.026,68.384 29.387,69.023C28.749,69.662 27.957,69.981 27.012,69.981H17.012ZM63.679,69.981H53.679C52.735,69.981 51.943,69.662 51.304,69.023C50.665,68.384 50.346,67.593 50.346,66.648C50.346,65.704 50.665,64.912 51.304,64.273C51.943,63.634 52.735,63.315 53.679,63.315H63.679V53.315C63.679,52.37 63.999,51.579 64.637,50.94C65.276,50.301 66.068,49.981 67.012,49.981C67.957,49.981 68.748,50.301 69.387,50.94C70.026,51.579 70.346,52.37 70.346,53.315V63.315C70.346,65.148 69.693,66.718 68.387,68.023C67.082,69.329 65.512,69.981 63.679,69.981ZM10.346,26.648V16.648C10.346,14.815 10.998,13.245 12.304,11.94C13.61,10.634 15.179,9.981 17.012,9.981H27.012C27.957,9.981 28.749,10.301 29.387,10.94C30.026,11.579 30.346,12.37 30.346,13.315C30.346,14.259 30.026,15.051 29.387,15.69C28.749,16.329 27.957,16.648 27.012,16.648H17.012V26.648C17.012,27.593 16.693,28.384 16.054,29.023C15.415,29.662 14.623,29.981 13.679,29.981C12.735,29.981 11.943,29.662 11.304,29.023C10.665,28.384 10.346,27.593 10.346,26.648ZM63.679,26.648V16.648H53.679C52.735,16.648 51.943,16.329 51.304,15.69C50.665,15.051 50.346,14.259 50.346,13.315C50.346,12.37 50.665,11.579 51.304,10.94C51.943,10.301 52.735,9.981 53.679,9.981H63.679C65.512,9.981 67.082,10.634 68.387,11.94C69.693,13.245 70.346,14.815 70.346,16.648V26.648C70.346,27.593 70.026,28.384 69.387,29.023C68.748,29.662 67.957,29.981 67.012,29.981C66.068,29.981 65.276,29.662 64.637,29.023C63.999,28.384 63.679,27.593 63.679,26.648Z"
|
||||
android:fillColor="#BCF0B4"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 886 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#235024</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">HemoAnalysis</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.HemoAnalysis" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">111.88.154.194</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
@@ -0,0 +1,10 @@
|
||||
plugins {
|
||||
alias(libs.plugins.androidMultiplatformLibrary) apply false
|
||||
alias(libs.plugins.androidApplication) apply false
|
||||
alias(libs.plugins.androidLibrary) apply false
|
||||
alias(libs.plugins.composeMultiplatform) apply false
|
||||
alias(libs.plugins.composeCompiler) apply false
|
||||
alias(libs.plugins.kotlinMultiplatform) apply false
|
||||
alias(libs.plugins.kotlinAndroid) apply false
|
||||
alias(libs.plugins.androidLint) apply false
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,63 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidMultiplatformLibrary)
|
||||
alias(libs.plugins.kotlinxSerialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
android {
|
||||
namespace = "ru.tanexc.hemoanalysis"
|
||||
compileSdk {
|
||||
version = release(36)
|
||||
}
|
||||
minSdk = 26
|
||||
}
|
||||
|
||||
listOf(
|
||||
iosArm64(),
|
||||
iosSimulatorArm64()
|
||||
).forEach { iosTarget ->
|
||||
iosTarget.binaries.framework {
|
||||
baseName = "datakit"
|
||||
isStatic = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api(projects.domain)
|
||||
api(projects.core.database)
|
||||
api(projects.core.network)
|
||||
|
||||
// settings store
|
||||
implementation(libs.multiplatform.settings)
|
||||
implementation(libs.multiplatform.settings.serialization)
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
|
||||
// koin
|
||||
implementation(libs.koin.core)
|
||||
|
||||
implementation(libs.kotlinx.datetime)
|
||||
}
|
||||
}
|
||||
|
||||
androidMain {
|
||||
dependencies {
|
||||
// datastore
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
}
|
||||
}
|
||||
|
||||
iosMain {
|
||||
dependencies {
|
||||
// datastore
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,33 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import ru.tanexc.hemoanalysis.Constants.INTERNAL_MODELS_DIR
|
||||
import ru.tanexc.hemoanalysis.domain.repository.ModelRepository
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetLatestModelFileUseCase
|
||||
import java.io.File
|
||||
|
||||
internal actual class GetLatestModelFileUseCaseImpl : GetLatestModelFileUseCase, KoinComponent {
|
||||
private val context: Context by inject()
|
||||
private val modelRepository: ModelRepository by inject()
|
||||
|
||||
actual override operator fun invoke(): ByteArray? {
|
||||
val filename = modelRepository.getCurrentModelInfo()?.filename?: return null
|
||||
|
||||
val modelsDir = File(context.filesDir, INTERNAL_MODELS_DIR)
|
||||
|
||||
if (!modelsDir.exists()) return null
|
||||
|
||||
val modelFile = File(modelsDir, filename)
|
||||
|
||||
if (!modelFile.exists()) return null
|
||||
|
||||
Log.i("GetLatestModelFileUseCase", "get model bytearray ${modelFile.absolutePath}")
|
||||
|
||||
return modelFile.inputStream().buffered().use { inputStream ->
|
||||
inputStream.readBytes()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import ru.tanexc.hemoanalysis.domain.repository.ModelRepository
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetModelPathUseCase
|
||||
import kotlin.getValue
|
||||
|
||||
actual class GetModelPathUseCaseImpl actual constructor() : GetModelPathUseCase, KoinComponent {
|
||||
private val modelRepository: ModelRepository by inject()
|
||||
|
||||
actual override fun invoke(): String? {
|
||||
return modelRepository.getCurrentModelInfo()?.filename
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import ru.tanexc.hemoanalysis.Constants.INTERNAL_MODELS_DIR
|
||||
import ru.tanexc.hemoanalysis.domain.model.Size
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.BytesReader
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.SaveModelFileUseCase
|
||||
import java.io.File
|
||||
|
||||
internal actual class SaveModelFileUseCaseImpl : SaveModelFileUseCase, KoinComponent {
|
||||
val context: Context by inject()
|
||||
|
||||
actual override suspend operator fun invoke(
|
||||
filename: String,
|
||||
bytesReader: BytesReader
|
||||
): Size {
|
||||
val modelsDir = File(context.filesDir, INTERNAL_MODELS_DIR)
|
||||
modelsDir.mkdirs()
|
||||
|
||||
Log.i("SaveModelFileUseCase", "saving model into ${modelsDir.absolutePath}")
|
||||
|
||||
val modelFile = File(modelsDir, filename)
|
||||
|
||||
modelFile.outputStream().buffered().use { output ->
|
||||
var bytes = bytesReader.readNext(BUFFER_SIZE)
|
||||
|
||||
while (bytes.isNotEmpty()) {
|
||||
output.write(bytes)
|
||||
bytes = bytesReader.readNext(BUFFER_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("SaveModelFileUseCase", "model saved into file $modelFile ${modelFile.length()}")
|
||||
|
||||
return Size(modelFile.length().toInt())
|
||||
}
|
||||
|
||||
private val BUFFER_SIZE = 1024 * 1024
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.tanexc.hemoanalysis
|
||||
|
||||
internal object Constants {
|
||||
const val INTERNAL_MODELS_DIR = "models"
|
||||
const val DEFAULT_MODEL_NAME = "model.ort"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package ru.tanexc.hemoanalysis.di
|
||||
|
||||
import org.koin.core.context.loadKoinModules
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
import ru.tanexc.hemoanalysis.repository.AnalysisParamsRepositoryImpl
|
||||
import ru.tanexc.hemoanalysis.repository.ModelRepositoryImpl
|
||||
import ru.tanexc.hemoanalysis.core.database.databaseModule
|
||||
import ru.tanexc.hemoanalysis.core.network.di.networkModule
|
||||
import ru.tanexc.hemoanalysis.domain.repository.AnalysisParamsRepository
|
||||
import ru.tanexc.hemoanalysis.domain.repository.ModelRepository
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetLatestModelFileUseCase
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetModelPathUseCase
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.SaveModelFileUseCase
|
||||
import ru.tanexc.hemoanalysis.usecase.GetLatestModelFileUseCaseImpl
|
||||
import ru.tanexc.hemoanalysis.usecase.GetModelPathUseCaseImpl
|
||||
import ru.tanexc.hemoanalysis.usecase.SaveModelFileUseCaseImpl
|
||||
|
||||
val dataModule = module {
|
||||
loadKoinModules(listOf(databaseModule, networkModule))
|
||||
singleOf(::GetModelPathUseCaseImpl) bind GetModelPathUseCase::class
|
||||
singleOf(::SaveModelFileUseCaseImpl) bind SaveModelFileUseCase::class
|
||||
singleOf(::GetLatestModelFileUseCaseImpl) bind GetLatestModelFileUseCase::class
|
||||
|
||||
single<AnalysisParamsRepository> {
|
||||
AnalysisParamsRepositoryImpl(
|
||||
settings = get(named("AnalysisParamsSettings"))
|
||||
)
|
||||
}
|
||||
|
||||
single<ModelRepository> {
|
||||
ModelRepositoryImpl(
|
||||
settings = get(named("ModelInfoSettings")),
|
||||
api = get(),
|
||||
saveModelFileUseCase = get()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package ru.tanexc.hemoanalysis.repository
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
import com.russhwolf.settings.float
|
||||
import com.russhwolf.settings.int
|
||||
import ru.tanexc.hemoanalysis.domain.repository.AnalysisParamsRepository
|
||||
|
||||
internal class AnalysisParamsRepositoryImpl(
|
||||
settings: Settings
|
||||
): AnalysisParamsRepository {
|
||||
override var confThreshold by settings.float("conf_threshold", 0.5f)
|
||||
override var iouThreshold by settings.float("iou_threshold", 0.5f)
|
||||
override var preprocessVariant by settings.int("preprocess_variant", 0)
|
||||
override var roiVariant by settings.int("roi_variant", 0)
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package ru.tanexc.hemoanalysis.repository
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
import com.russhwolf.settings.string
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.format
|
||||
import kotlinx.datetime.format.DateTimeFormat
|
||||
import kotlinx.datetime.format.char
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koin.core.logger.Logger
|
||||
import ru.tanexc.hemoanalysis.core.network.ModelApi
|
||||
import ru.tanexc.hemoanalysis.core.network.RequestState
|
||||
import ru.tanexc.hemoanalysis.domain.State
|
||||
import ru.tanexc.hemoanalysis.domain.model.ModelInfo
|
||||
import ru.tanexc.hemoanalysis.domain.model.Size
|
||||
import ru.tanexc.hemoanalysis.domain.repository.ModelRepository
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.SaveModelFileUseCase
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
internal class ModelRepositoryImpl(
|
||||
settings: Settings,
|
||||
private val api: ModelApi,
|
||||
private val saveModelFileUseCase: SaveModelFileUseCase,
|
||||
) : ModelRepository {
|
||||
private var currentModelInfo: String by settings.string("current_model_info", "")
|
||||
private var latestModelInfo: String by settings.string("latest_model_info", "")
|
||||
private var apiKey: String by settings.string("api_key", "")
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
override fun getLatestModelInfo(): ModelInfo? = runCatching {
|
||||
json.decodeFromString<ModelInfo>(latestModelInfo)
|
||||
}.getOrNull()
|
||||
|
||||
override fun getCurrentModelInfo(): ModelInfo? = runCatching {
|
||||
json.decodeFromString<ModelInfo>(currentModelInfo)
|
||||
}.getOrNull()
|
||||
|
||||
override fun updateModelInfo(): Flow<State<ModelInfo?>> = flow {
|
||||
emit(State.Loading)
|
||||
when (val state = api.getLatestModelInfo(apiKey)) {
|
||||
is RequestState.Error -> emit(State.Error)
|
||||
is RequestState.Success -> {
|
||||
val timeNow = nowFormatted()
|
||||
val meta = state.data
|
||||
val currentInfo = getCurrentModelInfo()?.copy(
|
||||
lastUpdateTime = timeNow
|
||||
)
|
||||
currentModelInfo = currentInfo.asString()
|
||||
|
||||
if (meta.version.isBiggerVersion(currentInfo?.version ?: "")) {
|
||||
val modelInfo = ModelInfo(
|
||||
version = meta.version,
|
||||
lastUpdateTime = timeNow,
|
||||
createdAt = meta.createdAt.toDisplayDateTime(),
|
||||
filename = meta.filename,
|
||||
isLatest = true,
|
||||
size = Size.ZERO
|
||||
)
|
||||
|
||||
currentModelInfo = currentInfo?.copy(
|
||||
isLatest = false
|
||||
).asString()
|
||||
|
||||
latestModelInfo = modelInfo.asString()
|
||||
emit(State.Success(modelInfo))
|
||||
} else {
|
||||
emit(State.Success(currentInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadLatestModel(): Flow<State<ModelInfo?>> = flow {
|
||||
emit(State.Loading)
|
||||
val latestInfo = getLatestModelInfo()
|
||||
if (latestInfo == null) {
|
||||
emit(State.Error)
|
||||
return@flow
|
||||
}
|
||||
|
||||
val state = api.downloadLatestModel(
|
||||
version = latestInfo.version,
|
||||
filename = latestInfo.filename,
|
||||
apiKey = apiKey
|
||||
)
|
||||
|
||||
when (state) {
|
||||
is RequestState.Error -> emit(State.Error)
|
||||
is RequestState.Success -> {
|
||||
val savedSize = saveModelFileUseCase(latestInfo.filename, state.data.bytesReader)
|
||||
|
||||
val currentInfo = latestInfo.copy(
|
||||
lastUpdateTime = nowFormatted(),
|
||||
isLatest = true,
|
||||
size = savedSize
|
||||
)
|
||||
|
||||
currentModelInfo = currentInfo.asString()
|
||||
emit(State.Success(currentInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getKeyApi(): String = apiKey
|
||||
|
||||
override fun updateApiKey(apiKey: String) {
|
||||
this.apiKey = apiKey
|
||||
}
|
||||
|
||||
private fun String.toVersionParts(): List<Int> {
|
||||
return split(".").map { part ->
|
||||
part.toIntOrNull() ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.isBiggerVersion(
|
||||
second: String
|
||||
): Boolean {
|
||||
val firstParts = this.toVersionParts()
|
||||
val secondParts = second.toVersionParts()
|
||||
|
||||
val maxSize = maxOf(firstParts.size, secondParts.size)
|
||||
|
||||
for (index in 0 until maxSize) {
|
||||
val firstValue = firstParts.getOrElse(index) { 0 }
|
||||
val secondValue = secondParts.getOrElse(index) { 0 }
|
||||
|
||||
if (firstValue > secondValue) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun ModelInfo?.asString() = this?.let(json::encodeToString) ?: ""
|
||||
|
||||
private fun nowFormatted(): String {
|
||||
return Clock.System.now().toDisplayDateTime()
|
||||
}
|
||||
|
||||
private fun String.toDisplayDateTime(): String {
|
||||
if (runCatching { displayDateTimeFormat.parse(this) }.isSuccess) return this
|
||||
|
||||
return runCatching {
|
||||
Instant.parse(this).toDisplayDateTime()
|
||||
}.getOrElse {
|
||||
runCatching {
|
||||
LocalDateTime.parse(this).toDisplayDateTime()
|
||||
}.getOrDefault(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Instant.toDisplayDateTime(): String {
|
||||
return toLocalDateTime(TimeZone.currentSystemDefault()).toDisplayDateTime()
|
||||
}
|
||||
|
||||
private fun LocalDateTime.toDisplayDateTime(): String {
|
||||
return format(displayDateTimeFormat)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val displayDateTimeFormat: DateTimeFormat<LocalDateTime> = LocalDateTime.Format {
|
||||
day()
|
||||
char('.')
|
||||
monthNumber()
|
||||
char('.')
|
||||
year()
|
||||
char(' ')
|
||||
hour()
|
||||
char(':')
|
||||
minute()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetLatestModelFileUseCase
|
||||
|
||||
internal expect class GetLatestModelFileUseCaseImpl(): GetLatestModelFileUseCase {
|
||||
override operator fun invoke(): ByteArray?
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetModelPathUseCase
|
||||
|
||||
expect class GetModelPathUseCaseImpl() : GetModelPathUseCase {
|
||||
override fun invoke(): String?
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import ru.tanexc.hemoanalysis.domain.model.Size
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.BytesReader
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.SaveModelFileUseCase
|
||||
|
||||
internal expect class SaveModelFileUseCaseImpl(): SaveModelFileUseCase {
|
||||
override suspend fun invoke(filename: String, bytesReader: BytesReader): Size
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.readBytes
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import platform.Foundation.NSApplicationSupportDirectory
|
||||
import platform.Foundation.NSData
|
||||
import platform.Foundation.NSFileManager
|
||||
import platform.Foundation.NSSearchPathForDirectoriesInDomains
|
||||
import platform.Foundation.NSUserDomainMask
|
||||
import platform.Foundation.dataWithContentsOfFile
|
||||
import ru.tanexc.hemoanalysis.Constants.INTERNAL_MODELS_DIR
|
||||
import ru.tanexc.hemoanalysis.domain.repository.ModelRepository
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetLatestModelFileUseCase
|
||||
import kotlin.getValue
|
||||
|
||||
internal actual class GetLatestModelFileUseCaseImpl : GetLatestModelFileUseCase, KoinComponent {
|
||||
private val modelRepository: ModelRepository by inject()
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual override operator fun invoke(): ByteArray? {
|
||||
val filename = modelRepository.getCurrentModelInfo()?.filename?: return null
|
||||
|
||||
val appSupportDir = getApplicationSupportDir()
|
||||
val modelFilePath = "$appSupportDir/$INTERNAL_MODELS_DIR/$filename"
|
||||
|
||||
if (!NSFileManager.defaultManager.fileExistsAtPath(modelFilePath)) return null
|
||||
|
||||
val data = NSData.dataWithContentsOfFile(modelFilePath) ?: return null
|
||||
|
||||
return data.bytes?.readBytes(data.length.toInt())
|
||||
}
|
||||
|
||||
private fun getApplicationSupportDir(): String {
|
||||
val paths = NSSearchPathForDirectoriesInDomains(
|
||||
directory = NSApplicationSupportDirectory,
|
||||
domainMask = NSUserDomainMask,
|
||||
expandTilde = true
|
||||
)
|
||||
|
||||
return paths.first() as? String ?: error("Application Support directory not found")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import platform.Foundation.NSApplicationSupportDirectory
|
||||
import platform.Foundation.NSFileManager
|
||||
import platform.Foundation.NSSearchPathForDirectoriesInDomains
|
||||
import platform.Foundation.NSUserDomainMask
|
||||
import ru.tanexc.hemoanalysis.Constants.INTERNAL_MODELS_DIR
|
||||
import ru.tanexc.hemoanalysis.domain.repository.ModelRepository
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetModelPathUseCase
|
||||
import kotlin.getValue
|
||||
|
||||
actual class GetModelPathUseCaseImpl actual constructor() : GetModelPathUseCase, KoinComponent {
|
||||
private val modelRepository: ModelRepository by inject()
|
||||
|
||||
actual override fun invoke(): String? {
|
||||
val filename = modelRepository.getCurrentModelInfo()?.filename ?: return null
|
||||
val modelPath = "${getApplicationSupportDir()}/$INTERNAL_MODELS_DIR/$filename"
|
||||
return modelPath.takeIf { NSFileManager.defaultManager.fileExistsAtPath(it) }
|
||||
}
|
||||
|
||||
private fun getApplicationSupportDir(): String {
|
||||
val paths = NSSearchPathForDirectoriesInDomains(
|
||||
directory = NSApplicationSupportDirectory,
|
||||
domainMask = NSUserDomainMask,
|
||||
expandTilde = true
|
||||
)
|
||||
|
||||
return paths.first() as? String ?: error("Application Support directory not found")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import kotlinx.cinterop.BetaInteropApi
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.allocArrayOf
|
||||
import kotlinx.cinterop.memScoped
|
||||
import platform.Foundation.NSApplicationSupportDirectory
|
||||
import platform.Foundation.NSData
|
||||
import platform.Foundation.NSFileHandle
|
||||
import platform.Foundation.NSFileManager
|
||||
import platform.Foundation.NSSearchPathForDirectoriesInDomains
|
||||
import platform.Foundation.NSUserDomainMask
|
||||
import platform.Foundation.closeFile
|
||||
import platform.Foundation.create
|
||||
import platform.Foundation.fileHandleForWritingAtPath
|
||||
import platform.Foundation.seekToEndOfFile
|
||||
import platform.Foundation.writeData
|
||||
import ru.tanexc.hemoanalysis.Constants.INTERNAL_MODELS_DIR
|
||||
import ru.tanexc.hemoanalysis.domain.model.Size
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.BytesReader
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.SaveModelFileUseCase
|
||||
|
||||
internal actual class SaveModelFileUseCaseImpl actual constructor() :
|
||||
SaveModelFileUseCase {
|
||||
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
|
||||
actual override suspend operator fun invoke(
|
||||
filename: String,
|
||||
bytesReader: BytesReader
|
||||
): Size {
|
||||
val appSupportDir = getApplicationSupportDir()
|
||||
val modelDir = "$appSupportDir/$INTERNAL_MODELS_DIR"
|
||||
NSFileManager.defaultManager.createDirectoryAtPath(
|
||||
path = modelDir,
|
||||
withIntermediateDirectories = true,
|
||||
attributes = null,
|
||||
error = null
|
||||
)
|
||||
|
||||
val filePath = "$modelDir/$filename"
|
||||
if (NSFileManager.defaultManager.fileExistsAtPath(filePath)) {
|
||||
NSFileManager.defaultManager.removeItemAtPath(filePath, error = null)
|
||||
}
|
||||
check(
|
||||
NSFileManager.defaultManager.createFileAtPath(
|
||||
path = filePath,
|
||||
contents = null,
|
||||
attributes = null
|
||||
)
|
||||
) { "Cannot create file at $filePath" }
|
||||
|
||||
val handle = NSFileHandle.fileHandleForWritingAtPath(filePath)
|
||||
?: error("Cannot open file for writing at $filePath")
|
||||
var totalBytes = 0
|
||||
try {
|
||||
handle.seekToEndOfFile()
|
||||
|
||||
var bytes = bytesReader.readNext(BUFFER_SIZE)
|
||||
while (bytes.isNotEmpty()) {
|
||||
handle.writeData(bytes.toNSData())
|
||||
totalBytes += bytes.size
|
||||
bytes = bytesReader.readNext(BUFFER_SIZE)
|
||||
}
|
||||
} finally {
|
||||
handle.closeFile()
|
||||
}
|
||||
|
||||
return Size(totalBytes)
|
||||
}
|
||||
|
||||
private fun getApplicationSupportDir(): String {
|
||||
val paths = NSSearchPathForDirectoriesInDomains(
|
||||
directory = NSApplicationSupportDirectory,
|
||||
domainMask = NSUserDomainMask,
|
||||
expandTilde = true
|
||||
)
|
||||
|
||||
return paths.first() as? String ?: error("Application Support directory not found")
|
||||
}
|
||||
|
||||
private val BUFFER_SIZE = 1024 * 1024
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
|
||||
private fun ByteArray.toNSData(): NSData = memScoped {
|
||||
NSData.create(
|
||||
bytes = allocArrayOf(this@toNSData),
|
||||
length = this@toNSData.size.toULong()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,56 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidMultiplatformLibrary)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
android {
|
||||
namespace = "ru.tanexc.hemoanalysis.core.database"
|
||||
compileSdk {
|
||||
version = release(36)
|
||||
}
|
||||
minSdk = 26
|
||||
}
|
||||
|
||||
listOf(
|
||||
iosArm64(),
|
||||
iosSimulatorArm64()
|
||||
).forEach { iosTarget ->
|
||||
iosTarget.binaries.framework {
|
||||
baseName = "databasekit"
|
||||
isStatic = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
// settings store
|
||||
implementation(libs.multiplatform.settings)
|
||||
implementation(libs.multiplatform.settings.serialization)
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
|
||||
// koin
|
||||
implementation(libs.koin.core)
|
||||
}
|
||||
}
|
||||
|
||||
androidMain {
|
||||
dependencies {
|
||||
// datastore
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
}
|
||||
}
|
||||
|
||||
iosMain {
|
||||
dependencies {
|
||||
// datastore
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,12 @@
|
||||
package ru.tanexc.hemoanalysis.core.database
|
||||
|
||||
import android.content.Context
|
||||
import com.russhwolf.settings.Settings
|
||||
import com.russhwolf.settings.SharedPreferencesSettings
|
||||
import org.koin.core.scope.Scope
|
||||
|
||||
internal actual fun Scope.createAnalysisParamsSettings(): Settings {
|
||||
return SharedPreferencesSettings(
|
||||
delegate = get<Context>().getSharedPreferences("analysis_params_settings", Context.MODE_PRIVATE)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.tanexc.hemoanalysis.core.database
|
||||
|
||||
import android.content.Context
|
||||
import com.russhwolf.settings.Settings
|
||||
import com.russhwolf.settings.SharedPreferencesSettings
|
||||
import org.koin.core.scope.Scope
|
||||
|
||||
internal actual fun Scope.createModelInfoSettings(): Settings {
|
||||
return SharedPreferencesSettings(
|
||||
delegate = get<Context>().getSharedPreferences("model_info_settings", Context.MODE_PRIVATE)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.tanexc.hemoanalysis.core.database
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
import org.koin.core.scope.Scope
|
||||
|
||||
internal expect fun Scope.createAnalysisParamsSettings(): Settings
|
||||
@@ -0,0 +1,10 @@
|
||||
package ru.tanexc.hemoanalysis.core.database
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
import org.koin.core.qualifier.StringQualifier
|
||||
import org.koin.dsl.module
|
||||
|
||||
val databaseModule = module {
|
||||
single<Settings>(qualifier = StringQualifier("AnalysisParamsSettings")) { createAnalysisParamsSettings() }
|
||||
single<Settings>(qualifier = StringQualifier("ModelInfoSettings")) { createModelInfoSettings() }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.tanexc.hemoanalysis.core.database
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
import org.koin.core.scope.Scope
|
||||
|
||||
internal expect fun Scope.createModelInfoSettings(): Settings
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.tanexc.hemoanalysis.core.database
|
||||
|
||||
import com.russhwolf.settings.NSUserDefaultsSettings
|
||||
import com.russhwolf.settings.Settings
|
||||
import org.koin.core.scope.Scope
|
||||
import platform.Foundation.NSUserDefaults
|
||||
|
||||
internal actual fun Scope.createAnalysisParamsSettings(): Settings {
|
||||
return NSUserDefaultsSettings(
|
||||
delegate = NSUserDefaults.standardUserDefaults
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.tanexc.hemoanalysis.core.database
|
||||
|
||||
import com.russhwolf.settings.NSUserDefaultsSettings
|
||||
import com.russhwolf.settings.Settings
|
||||
import org.koin.core.scope.Scope
|
||||
import platform.Foundation.NSUserDefaults
|
||||
|
||||
internal actual fun Scope.createModelInfoSettings(): Settings {
|
||||
return NSUserDefaultsSettings(
|
||||
delegate = NSUserDefaults.standardUserDefaults
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,58 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidMultiplatformLibrary)
|
||||
alias(libs.plugins.kotlinxSerialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
android {
|
||||
namespace = "ru.tanexc.hemoanalysis.navigation"
|
||||
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
|
||||
compileSdk {
|
||||
version = release(36)
|
||||
}
|
||||
minSdk = 26
|
||||
}
|
||||
|
||||
listOf(
|
||||
iosArm64(),
|
||||
iosSimulatorArm64()
|
||||
).forEach { iosTarget ->
|
||||
iosTarget.binaries.framework {
|
||||
baseName = "navigationkit"
|
||||
isStatic = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
|
||||
implementation(libs.compose.foundation)
|
||||
|
||||
// decompose
|
||||
implementation(libs.decompose)
|
||||
implementation(libs.decompose.extensions.compose)
|
||||
implementation(libs.decompose.extensions.compose.experimental)
|
||||
implementation(libs.essenty.lifecycle)
|
||||
|
||||
// koin
|
||||
implementation(libs.koin.core)
|
||||
}
|
||||
}
|
||||
|
||||
androidMain {
|
||||
dependencies {
|
||||
implementation(libs.androidx.activity.ktx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,16 @@
|
||||
package ru.tanexc.hemoanalysis.navigation
|
||||
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
||||
abstract class BaseComponent(
|
||||
val context: ComponentContext,
|
||||
val onNavigate: (Config) -> Unit,
|
||||
val onBack: () -> Unit
|
||||
): ComponentContext by context {
|
||||
val componentScope: CoroutineScope
|
||||
get() = CoroutineScope(SupervisorJob() + Dispatchers.IO).withLifecycle(lifecycle)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package ru.tanexc.hemoanalysis.navigation
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed class Config(
|
||||
val navLabelResId: String,
|
||||
val navIconResId: String
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data object Analysis: Config(
|
||||
navLabelResId = "analysis_label",
|
||||
navIconResId = "analysis",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data object Settings: Config(
|
||||
navLabelResId = "settings_label",
|
||||
navIconResId = "settings",
|
||||
)
|
||||
|
||||
companion object {
|
||||
val navEntries: List<Config> = listOf(Analysis, Settings)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package ru.tanexc.hemoanalysis.navigation
|
||||
|
||||
import com.arkivanov.essenty.lifecycle.Lifecycle
|
||||
import com.arkivanov.essenty.lifecycle.doOnDestroy
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koin.mp.KoinPlatform.getKoin
|
||||
|
||||
fun CoroutineScope.withLifecycle(lifecycle: Lifecycle): CoroutineScope {
|
||||
lifecycle.doOnDestroy(::cancel)
|
||||
return this
|
||||
}
|
||||
|
||||
inline fun <reified T : BaseComponent> getComponent(vararg parameters: Any?) = getKoin().get<T> {
|
||||
parametersOf(*parameters)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,52 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidMultiplatformLibrary)
|
||||
alias(libs.plugins.kotlinxSerialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
android {
|
||||
namespace = "ru.tanexc.hemoanalysis.core.network"
|
||||
compileSdk {
|
||||
version = release(36)
|
||||
}
|
||||
minSdk = 26
|
||||
}
|
||||
|
||||
listOf(
|
||||
iosArm64(),
|
||||
iosSimulatorArm64()
|
||||
).forEach { iosTarget ->
|
||||
iosTarget.binaries.framework {
|
||||
baseName = "nwtworkKit"
|
||||
isStatic = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api(projects.domain)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
|
||||
// koin
|
||||
implementation(libs.koin.core)
|
||||
|
||||
// ktor
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.content.negotiation)
|
||||
implementation(libs.ktor.client.logging)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
androidMain.dependencies {
|
||||
implementation(libs.ktor.client.okhttp)
|
||||
}
|
||||
|
||||
iosMain.dependencies {
|
||||
implementation(libs.ktor.client.darwin)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,10 @@
|
||||
package ru.tanexc.hemoanalysis.core.network.util
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
|
||||
internal actual fun PlatformHttpClient(declaration: HttpClientConfig<*>.() -> Unit): HttpClient = HttpClient(
|
||||
engineFactory = OkHttp,
|
||||
block = declaration,
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
package ru.tanexc.hemoanalysis.core.network
|
||||
|
||||
internal const val DEFAULT_BASE_URL = "http://111.88.154.194:8000"
|
||||
@@ -0,0 +1,67 @@
|
||||
package ru.tanexc.hemoanalysis.core.network
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.http.isSuccess
|
||||
import io.ktor.util.logging.KtorSimpleLogger
|
||||
import io.ktor.utils.io.availableForRead
|
||||
import io.ktor.utils.io.readBuffer
|
||||
import kotlinx.io.readByteArray
|
||||
import ru.tanexc.hemoanalysis.core.network.response.ModelDownloadResponse
|
||||
import ru.tanexc.hemoanalysis.core.network.response.ModelMetaResponse
|
||||
import ru.tanexc.hemoanalysis.core.network.util.get
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.BytesReader
|
||||
import kotlin.math.log
|
||||
import io.ktor.client.request.get as ktorGet
|
||||
|
||||
class ModelApi(
|
||||
private val httpClient: HttpClient,
|
||||
) {
|
||||
suspend fun getLatestModelInfo(apiKey: String): RequestState<ModelMetaResponse> =
|
||||
httpClient.get(url = "$DEFAULT_BASE_URL/models/latest") {
|
||||
header("X-API-KEY", apiKey)
|
||||
}
|
||||
|
||||
suspend fun downloadLatestModel(
|
||||
apiKey: String,
|
||||
version: String,
|
||||
filename: String
|
||||
): RequestState<ModelDownloadResponse> = runCatching {
|
||||
val response: HttpResponse =
|
||||
httpClient.ktorGet("$DEFAULT_BASE_URL/models/latest/download") {
|
||||
header("X-API-KEY", apiKey)
|
||||
parameter("version", version)
|
||||
parameter("filename", filename)
|
||||
contentType(ContentType.Application.OctetStream)
|
||||
}
|
||||
when {
|
||||
response.status.isSuccess() -> {
|
||||
val buffer = response.bodyAsChannel()
|
||||
val bytesReader = object : BytesReader {
|
||||
override suspend fun readNext(size: Int): ByteArray {
|
||||
return buffer.readBuffer(size).readByteArray()
|
||||
}
|
||||
}
|
||||
RequestState.Success(
|
||||
ModelDownloadResponse(
|
||||
bytesReader = bytesReader,
|
||||
totalBytes = buffer.availableForRead
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
response.status == HttpStatusCode.NotFound -> RequestState.Error("Model not found")
|
||||
else -> RequestState.Error(response.status.description)
|
||||
}
|
||||
}.getOrElse {
|
||||
RequestState.Error(
|
||||
message = it.message ?: "Request error: $DEFAULT_BASE_URL/models/latest/download"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.tanexc.hemoanalysis.core.network
|
||||
|
||||
sealed interface RequestState<out T> {
|
||||
data class Success<T>(val data: T): RequestState<T>
|
||||
|
||||
data class Error(val message: String): RequestState<Nothing>
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package ru.tanexc.hemoanalysis.core.network.di
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.logging.DEFAULT
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import io.ktor.client.plugins.logging.SIMPLE
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
import ru.tanexc.hemoanalysis.core.network.ModelApi
|
||||
import ru.tanexc.hemoanalysis.core.network.util.PlatformHttpClient
|
||||
|
||||
val networkModule = module {
|
||||
single<HttpClient> {
|
||||
PlatformHttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json(Json { ignoreUnknownKeys = true })
|
||||
}
|
||||
|
||||
install(Logging) {
|
||||
level = LogLevel.HEADERS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
singleOf(::ModelApi)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package ru.tanexc.hemoanalysis.core.network.response
|
||||
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.BytesReader
|
||||
|
||||
data class ModelDownloadResponse(
|
||||
val bytesReader: BytesReader,
|
||||
val totalBytes: Int
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.tanexc.hemoanalysis.core.network.response
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ModelMetaResponse(
|
||||
val version: String,
|
||||
@SerialName("created_at")
|
||||
val createdAt: String,
|
||||
val filename: String
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.tanexc.hemoanalysis.core.network.util
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import org.koin.core.scope.Scope
|
||||
|
||||
internal expect fun PlatformHttpClient(declaration: HttpClientConfig<*>.() -> Unit): HttpClient
|
||||
@@ -0,0 +1,28 @@
|
||||
package ru.tanexc.hemoanalysis.core.network.util
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.http.isSuccess
|
||||
import ru.tanexc.hemoanalysis.core.network.RequestState
|
||||
|
||||
internal suspend inline fun <reified T> HttpClient.get(
|
||||
url: String,
|
||||
block: HttpRequestBuilder.() -> Unit = {}
|
||||
): RequestState<T> = runCatching {
|
||||
get(
|
||||
urlString = url,
|
||||
block = block
|
||||
).handleResponse<T>()
|
||||
}.getOrElse { RequestState.Error(message = it.message ?: "Request error: $url") }
|
||||
|
||||
internal suspend inline fun <reified T> HttpResponse.handleResponse(): RequestState<T> = when {
|
||||
status.isSuccess() -> RequestState.Success(body())
|
||||
else -> RequestState.Error(status.description)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package ru.tanexc.hemoanalysis.core.network.util
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.engine.darwin.Darwin
|
||||
|
||||
internal actual fun PlatformHttpClient(declaration: HttpClientConfig<*>.() -> Unit): HttpClient = HttpClient(
|
||||
engineFactory = Darwin,
|
||||
block = declaration
|
||||
)
|
||||
@@ -0,0 +1,129 @@
|
||||
@startuml
|
||||
left to right direction
|
||||
|
||||
actor "Работник фабрики" as FactoryWorker
|
||||
actor "Работник склада" as WarehouseWorker
|
||||
actor "Работник магазина" as ShopWorker
|
||||
actor "Администратор" as Admin
|
||||
|
||||
note left of FactoryWorker
|
||||
Отвечает за регистрацию
|
||||
произведенных товаров
|
||||
и организацию поставок
|
||||
end note
|
||||
|
||||
note left of WarehouseWorker
|
||||
Управляет складскими операциями:
|
||||
приемка, хранение, отгрузка
|
||||
end note
|
||||
|
||||
note left of ShopWorker
|
||||
Формирует заявки на товары
|
||||
и отслеживает доступность
|
||||
end note
|
||||
|
||||
note left of Admin
|
||||
Управляет пользователями,
|
||||
ролями и привязкой
|
||||
сотрудников к складам
|
||||
end note
|
||||
|
||||
rectangle "Система управления складом" {
|
||||
|
||||
usecase "Управление товарами" as UC_ManageProducts
|
||||
usecase "Управление категориями" as UC_ManageCategories
|
||||
usecase "Регистрация поставки" as UC_RegisterDelivery
|
||||
|
||||
usecase "Добавить товар" as UC_AddProduct
|
||||
usecase "Управление ассортиментом" as UC_Assortment
|
||||
usecase "Редактировать товар" as UC_EditProduct
|
||||
|
||||
usecase "Указать фабрику" as UC_SetFactory
|
||||
usecase "Указать дату поставки" as UC_SetDeliveryDate
|
||||
usecase "Указать товары" as UC_SetDeliveryProducts
|
||||
usecase "Указать склад" as UC_SetWarehouse
|
||||
|
||||
usecase "Подтверждение приемки" as UC_AcceptDelivery
|
||||
usecase "Подтвердить количество" as UC_ConfirmQuantity
|
||||
usecase "Проверить товар" as UC_CheckProduct
|
||||
usecase "Обновить остатки" as UC_UpdateStock
|
||||
|
||||
usecase "Обработка отгрузки" as UC_ProcessShipment
|
||||
usecase "Зафиксировать дату отгрузки" as UC_SetShipmentDate
|
||||
usecase "Подтвердить отгрузку" as UC_ConfirmShipment
|
||||
|
||||
usecase "Просмотр остатков" as UC_ViewStock
|
||||
usecase "Просмотр по категориям" as UC_ViewByCategories
|
||||
usecase "Просмотр по складам" as UC_ViewByWarehouses
|
||||
|
||||
usecase "Запрос отгрузки" as UC_RequestShipment
|
||||
usecase "Указать магазин" as UC_SetShop
|
||||
usecase "Указать количество" as UC_SetQuantity
|
||||
usecase "Выбрать товары" as UC_SelectProducts
|
||||
|
||||
usecase "Управление пользователями" as UC_ManageUsers
|
||||
usecase "Создать пользователя" as UC_CreateUser
|
||||
usecase "Редактировать пользователя" as UC_EditUser
|
||||
usecase "Логически удалить пользователя" as UC_DeleteUser
|
||||
|
||||
usecase "Управление ролями" as UC_ManageRoles
|
||||
usecase "Назначить роль" as UC_AssignRole
|
||||
|
||||
usecase "Управление привязкой к складам" as UC_ManageWarehouseLinks
|
||||
usecase "Привязать пользователя к складу" as UC_LinkUserWarehouse
|
||||
usecase "Отвязать пользователя от склада" as UC_UnlinkUserWarehouse
|
||||
usecase "Назначить владельца склада" as UC_AssignWarehouseOwner
|
||||
}
|
||||
|
||||
FactoryWorker --> UC_ManageProducts
|
||||
FactoryWorker --> UC_ManageCategories
|
||||
FactoryWorker --> UC_RegisterDelivery
|
||||
|
||||
WarehouseWorker --> UC_AcceptDelivery
|
||||
WarehouseWorker --> UC_ProcessShipment
|
||||
WarehouseWorker --> UC_ViewStock
|
||||
|
||||
ShopWorker --> UC_ViewStock
|
||||
ShopWorker --> UC_RequestShipment
|
||||
|
||||
Admin --> UC_ManageUsers
|
||||
Admin --> UC_ManageRoles
|
||||
Admin --> UC_ManageWarehouseLinks
|
||||
|
||||
UC_ManageProducts ..> UC_AddProduct : include
|
||||
UC_ManageProducts ..> UC_Assortment : include
|
||||
UC_ManageProducts ..> UC_EditProduct : include
|
||||
|
||||
UC_RegisterDelivery ..> UC_SetFactory : include
|
||||
UC_RegisterDelivery ..> UC_SetDeliveryDate : include
|
||||
UC_RegisterDelivery ..> UC_SetDeliveryProducts : include
|
||||
UC_RegisterDelivery ..> UC_SetWarehouse : include
|
||||
|
||||
UC_AcceptDelivery ..> UC_ConfirmQuantity : include
|
||||
UC_AcceptDelivery ..> UC_CheckProduct : include
|
||||
UC_AcceptDelivery ..> UC_SetWarehouse : include
|
||||
UC_AcceptDelivery ..> UC_UpdateStock : include
|
||||
|
||||
UC_ProcessShipment ..> UC_UpdateStock : include
|
||||
UC_ProcessShipment ..> UC_SetShipmentDate : include
|
||||
UC_ProcessShipment ..> UC_ConfirmShipment : include
|
||||
|
||||
UC_ViewStock ..> UC_ViewByCategories : include
|
||||
UC_ViewStock ..> UC_ViewByWarehouses : include
|
||||
|
||||
UC_RequestShipment ..> UC_SetShop : include
|
||||
UC_RequestShipment ..> UC_SetQuantity : include
|
||||
UC_RequestShipment ..> UC_SelectProducts : include
|
||||
UC_RequestShipment ..> UC_SetWarehouse : include
|
||||
|
||||
UC_ManageUsers ..> UC_CreateUser : include
|
||||
UC_ManageUsers ..> UC_EditUser : include
|
||||
UC_ManageUsers ..> UC_DeleteUser : include
|
||||
|
||||
UC_ManageRoles ..> UC_AssignRole : include
|
||||
|
||||
UC_ManageWarehouseLinks ..> UC_LinkUserWarehouse : include
|
||||
UC_ManageWarehouseLinks ..> UC_UnlinkUserWarehouse : include
|
||||
UC_ManageWarehouseLinks ..> UC_AssignWarehouseOwner : include
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,35 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidMultiplatformLibrary)
|
||||
alias(libs.plugins.kotlinxSerialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
androidLibrary {
|
||||
namespace = "ru.tanexc.hemoanalysis.domain"
|
||||
compileSdk {
|
||||
version = release(36)
|
||||
}
|
||||
minSdk = 26
|
||||
}
|
||||
|
||||
listOf(
|
||||
iosArm64(),
|
||||
iosSimulatorArm64()
|
||||
).forEach { iosTarget ->
|
||||
iosTarget.binaries.framework {
|
||||
baseName = "domainkit"
|
||||
isStatic = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package ru.tanexc.hemoanalysis.domain
|
||||
|
||||
sealed interface State<out T> {
|
||||
data object Loading: State<Nothing>
|
||||
|
||||
data class Success<T>(val data: T): State<T>
|
||||
|
||||
data object Error: State<Nothing>
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package ru.tanexc.hemoanalysis.domain.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ModelInfo(
|
||||
val version: String,
|
||||
val lastUpdateTime: String,
|
||||
val createdAt: String,
|
||||
val filename: String,
|
||||
val isLatest: Boolean,
|
||||
val size: Size
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
package ru.tanexc.hemoanalysis.domain.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class Size(
|
||||
val value: Int
|
||||
) {
|
||||
companion object {
|
||||
val ZERO get() = Size(0)
|
||||
}
|
||||
}
|
||||
|
||||
val Size.Kb: Float get() = value / 1024f
|
||||
val Size.Mb: Float get() = Kb / 1024f
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package ru.tanexc.hemoanalysis.domain.repository
|
||||
|
||||
interface AnalysisParamsRepository {
|
||||
var confThreshold: Float
|
||||
var iouThreshold: Float
|
||||
var preprocessVariant: Int
|
||||
var roiVariant: Int
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package ru.tanexc.hemoanalysis.domain.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import ru.tanexc.hemoanalysis.domain.State
|
||||
import ru.tanexc.hemoanalysis.domain.model.ModelInfo
|
||||
|
||||
interface ModelRepository {
|
||||
fun getLatestModelInfo(): ModelInfo?
|
||||
|
||||
fun getCurrentModelInfo(): ModelInfo?
|
||||
|
||||
fun updateModelInfo(): Flow<State<ModelInfo?>>
|
||||
|
||||
fun loadLatestModel(): Flow<State<ModelInfo?>>
|
||||
|
||||
fun getKeyApi(): String
|
||||
|
||||
fun updateApiKey(apiKey: String)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package ru.tanexc.hemoanalysis.domain.usecase
|
||||
|
||||
interface BytesReader {
|
||||
suspend fun readNext(size: Int): ByteArray
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package ru.tanexc.hemoanalysis.domain.usecase
|
||||
|
||||
interface GetLatestModelFileUseCase {
|
||||
operator fun invoke(): ByteArray?
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package ru.tanexc.hemoanalysis.domain.usecase
|
||||
|
||||
interface GetModelPathUseCase {
|
||||
operator fun invoke(): String?
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.tanexc.hemoanalysis.domain.usecase
|
||||
|
||||
import ru.tanexc.hemoanalysis.domain.model.Size
|
||||
|
||||
interface SaveModelFileUseCase {
|
||||
suspend operator fun invoke(filename: String, bytesReader: BytesReader): Size
|
||||
}
|
||||
@@ -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>
|
||||