first commit
This commit is contained in:
commit
e0d55e3087
|
|
@ -0,0 +1,15 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceTable">
|
||||
<option name="columnSorters">
|
||||
<list>
|
||||
<ColumnSorterState>
|
||||
<option name="column" value="Name" />
|
||||
<option name="order" value="ASCENDING" />
|
||||
</ColumnSorterState>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="StudioBotProjectSettings">
|
||||
<option name="shareContext" value="OptedIn" />
|
||||
</component>
|
||||
</project>
|
||||
Binary file not shown.
|
|
@ -0,0 +1,4 @@
|
|||
kotlin version: 2.0.21
|
||||
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
|
||||
1. Kotlin compile daemon is ready
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.kotlinx.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.livingai"
|
||||
compileSdk {
|
||||
version = release(36)
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.livingai"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.paging.common)
|
||||
implementation(libs.androidx.ui)
|
||||
val cameraxVersion = "1.5.0-alpha03"
|
||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||
implementation("androidx.cardview:cardview:1.0.0")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
//Splash Api
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("androidx.camera:camera-core:${cameraxVersion}")
|
||||
implementation("androidx.camera:camera-camera2:${cameraxVersion}")
|
||||
implementation("androidx.camera:camera-lifecycle:${cameraxVersion}")
|
||||
implementation("androidx.camera:camera-view:${cameraxVersion}")
|
||||
implementation("androidx.camera:camera-video:${cameraxVersion}")
|
||||
implementation("androidx.camera:camera-mlkit-vision:${cameraxVersion}")
|
||||
implementation("com.google.mlkit:object-detection:17.0.2")
|
||||
implementation("com.google.android.gms:play-services-mlkit-subject-segmentation:16.0.0-beta1")
|
||||
|
||||
//Koin
|
||||
implementation(libs.koin.android)
|
||||
implementation(libs.koin.androidx.compose)
|
||||
|
||||
//Navigation (Nav3 / Type-Safe Navigation)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
// Icons
|
||||
implementation("androidx.compose.material:material-icons-extended:1.7.5")
|
||||
|
||||
// OpenCSV
|
||||
implementation("com.opencsv:opencsv:5.7.1")
|
||||
|
||||
// Coil
|
||||
implementation("io.coil-kt:coil-compose:2.5.0")
|
||||
implementation("io.coil-kt:coil-video:2.5.0")
|
||||
|
||||
// Paging
|
||||
implementation("androidx.paging:paging-runtime:3.2.1")
|
||||
implementation("androidx.paging:paging-compose:3.2.1")
|
||||
|
||||
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.example.livingai
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.example.livingai", appContext.packageName)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||
|
||||
<!-- Permissions for Scoped Storage on Android 13+ -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
|
||||
<application
|
||||
android:name=".LivingAIApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/LivingAI.Starting.Theme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/LivingAI.Starting.Theme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.example.livingai
|
||||
|
||||
import android.app.Application
|
||||
import com.example.livingai.di.appModule
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.context.startKoin
|
||||
|
||||
class LivingAIApplication: Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
startKoin {
|
||||
androidLogger()
|
||||
androidContext(this@LivingAIApplication)
|
||||
modules(appModule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.example.livingai
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.example.livingai.pages.home.HomeViewModel
|
||||
import com.example.livingai.pages.navigation.NavGraph
|
||||
import com.example.livingai.ui.theme.LivingAITheme
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
val viewModel by viewModel<HomeViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
installSplashScreen().apply {
|
||||
setKeepOnScreenCondition {
|
||||
viewModel.splashCondition.value
|
||||
}
|
||||
}
|
||||
setContent {
|
||||
LivingAITheme {
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.auto(
|
||||
lightScrim = Color.Transparent.toArgb(),
|
||||
darkScrim = Color.Transparent.toArgb()
|
||||
),
|
||||
navigationBarStyle = SystemBarStyle.auto(
|
||||
lightScrim = Color.Transparent.toArgb(),
|
||||
darkScrim = Color.Transparent.toArgb()
|
||||
)
|
||||
)
|
||||
Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.background)) {
|
||||
val startDestination = viewModel.startDestination.value
|
||||
NavGraph(startDestination = startDestination)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.example.livingai.data.local
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.example.livingai.domain.model.AnimalProfile
|
||||
|
||||
class AnimalDataPagingSource(
|
||||
private val dataSource: CSVDataSource
|
||||
) : PagingSource<Int, AnimalProfile>() {
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, AnimalProfile>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
val anchorPage = state.closestPageToPosition(anchorPosition)
|
||||
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, AnimalProfile> {
|
||||
val page = params.key ?: 0
|
||||
|
||||
return try {
|
||||
// Since CSVDataSource reads all lines at once currently (simulated simple DB),
|
||||
// we will paginate the list in memory.
|
||||
val allProfiles = dataSource.getAllAnimalProfiles()
|
||||
|
||||
val start = page * params.loadSize
|
||||
val end = minOf(start + params.loadSize, allProfiles.size)
|
||||
|
||||
if (start >= allProfiles.size) {
|
||||
return LoadResult.Page(
|
||||
data = emptyList(),
|
||||
prevKey = if (page > 0) page - 1 else null,
|
||||
nextKey = null
|
||||
)
|
||||
}
|
||||
|
||||
val pagedData = allProfiles.subList(start, end)
|
||||
|
||||
LoadResult.Page(
|
||||
data = pagedData,
|
||||
prevKey = if (page > 0) page - 1 else null,
|
||||
nextKey = if (end < allProfiles.size) page + 1 else null
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LoadResult.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,412 @@
|
|||
package com.example.livingai.data.local
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import com.example.livingai.domain.model.*
|
||||
import com.example.livingai.domain.repository.business.DataSource
|
||||
import com.opencsv.CSVReader
|
||||
import com.opencsv.CSVWriter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.*
|
||||
|
||||
class CSVDataSource(
|
||||
private val context: Context,
|
||||
private val fileName: String
|
||||
) : DataSource {
|
||||
|
||||
private val folderName = "LivingAI"
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
private var cachedUri: Uri? = null
|
||||
|
||||
private suspend fun getCsvUri(): Uri = withContext(Dispatchers.IO) {
|
||||
cachedUri?.let { return@withContext it }
|
||||
|
||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
queryOrCreateCsvQ()
|
||||
} else {
|
||||
legacyGetOrCreateFile()
|
||||
}
|
||||
|
||||
cachedUri = uri
|
||||
return@withContext uri
|
||||
}
|
||||
|
||||
private fun queryOrCreateCsvQ(): Uri {
|
||||
val collection = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
|
||||
val projection = arrayOf(
|
||||
MediaStore.Files.FileColumns._ID,
|
||||
MediaStore.Files.FileColumns.DISPLAY_NAME,
|
||||
MediaStore.Files.FileColumns.RELATIVE_PATH
|
||||
)
|
||||
|
||||
val cursor = context.contentResolver.query(
|
||||
collection,
|
||||
projection,
|
||||
"${MediaStore.Files.FileColumns.DISPLAY_NAME}=?",
|
||||
arrayOf(fileName),
|
||||
null
|
||||
)
|
||||
|
||||
cursor?.use {
|
||||
val idCol = it.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
|
||||
val pathCol = it.getColumnIndexOrThrow(MediaStore.Files.FileColumns.RELATIVE_PATH)
|
||||
|
||||
while (it.moveToNext()) {
|
||||
val relPath = it.getString(pathCol) ?: ""
|
||||
if (relPath.contains(folderName)) {
|
||||
val id = it.getLong(idCol)
|
||||
return ContentUris.withAppendedId(collection, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create file if not found
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName)
|
||||
put(MediaStore.Files.FileColumns.MIME_TYPE, "text/csv")
|
||||
put(
|
||||
MediaStore.Files.FileColumns.RELATIVE_PATH,
|
||||
"${Environment.DIRECTORY_DOCUMENTS}/$folderName/"
|
||||
)
|
||||
}
|
||||
|
||||
return context.contentResolver.insert(collection, values)!!.also { uri ->
|
||||
writeHeader(uri)
|
||||
}
|
||||
}
|
||||
|
||||
private fun legacyGetOrCreateFile(): Uri {
|
||||
val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
|
||||
val sub = File(dir, folderName)
|
||||
if (!sub.exists()) sub.mkdirs()
|
||||
|
||||
val file = File(sub, fileName)
|
||||
if (!file.exists()) {
|
||||
file.createNewFile()
|
||||
writeHeaderLegacy(file)
|
||||
}
|
||||
return Uri.fromFile(file)
|
||||
}
|
||||
|
||||
private suspend fun readAllLines(): List<Array<String>> = mutex.withLock {
|
||||
val uri = getCsvUri()
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
val reader = CSVReader(InputStreamReader(input))
|
||||
val lines = reader.readAll()
|
||||
reader.close()
|
||||
if (lines.isNotEmpty() && lines[0].contentEquals(HEADER)) lines.drop(1)
|
||||
else lines
|
||||
} ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun writeAllLines(lines: List<Array<String>>) = mutex.withLock {
|
||||
val uri = getCsvUri()
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
context.contentResolver.openOutputStream(uri, "wt")?.use { out ->
|
||||
val writer = CSVWriter(OutputStreamWriter(out))
|
||||
writer.writeNext(HEADER)
|
||||
writer.writeAll(lines)
|
||||
writer.close()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeHeader(uri: Uri) {
|
||||
context.contentResolver.openOutputStream(uri, "wt")?.use { out ->
|
||||
CSVWriter(OutputStreamWriter(out)).use { it.writeNext(HEADER) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeHeaderLegacy(file: File) {
|
||||
CSVWriter(FileWriter(file)).use { it.writeNext(HEADER) }
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
// 3) PUBLIC API IMPLEMENTATION
|
||||
// --------------------------------------------------------------------------------------------
|
||||
|
||||
override fun getAnimalProfiles(): Flow<PagingData<AnimalProfile>> =
|
||||
Pager(PagingConfig(pageSize = 20)) {
|
||||
AnimalDataPagingSource(this)
|
||||
}.flow
|
||||
|
||||
suspend fun getAllAnimalProfiles(): List<AnimalProfile> {
|
||||
return readAllLines().mapNotNull(::parseAnimalProfile)
|
||||
}
|
||||
|
||||
override fun getAnimalDetails(animalId: String): Flow<AnimalDetails?> = flow {
|
||||
emit(parseAnimalDetails(readAllLines().find { it.getOrNull(INDEX_ID) == animalId }))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
override fun getAnimalRatings(animalId: String): Flow<AnimalRating?> = flow {
|
||||
emit(parseAnimalRating(readAllLines().find { it.getOrNull(INDEX_ID) == animalId }))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
override suspend fun setAnimalProfile(p: AnimalProfile) {
|
||||
val lines = readAllLines().toMutableList()
|
||||
val i = lines.indexOfFirst { it.getOrNull(INDEX_ID) == p.animalId }
|
||||
|
||||
if (i != -1) lines[i] = updateProfile(lines[i], p)
|
||||
else lines.add(createProfile(p))
|
||||
|
||||
writeAllLines(lines)
|
||||
}
|
||||
|
||||
override suspend fun setAnimalDetails(d: AnimalDetails) {
|
||||
val lines = readAllLines().toMutableList()
|
||||
val i = lines.indexOfFirst { it.getOrNull(INDEX_ID) == d.animalId }
|
||||
|
||||
if (i != -1) lines[i] = updateDetails(lines[i], d)
|
||||
else lines.add(createDetails(d))
|
||||
|
||||
writeAllLines(lines)
|
||||
}
|
||||
|
||||
override suspend fun setAnimalRatings(r: AnimalRating) {
|
||||
val lines = readAllLines().toMutableList()
|
||||
val i = lines.indexOfFirst { it.getOrNull(INDEX_ID) == r.animalId }
|
||||
|
||||
if (i != -1) lines[i] = updateRating(lines[i], r)
|
||||
else lines.add(createRating(r))
|
||||
|
||||
writeAllLines(lines)
|
||||
}
|
||||
|
||||
override suspend fun deleteAnimalProfile(animalId: String) {
|
||||
val lines = readAllLines().toMutableList()
|
||||
val i = lines.indexOfFirst { it.getOrNull(INDEX_ID) == animalId }
|
||||
if (i != -1) {
|
||||
lines.removeAt(i)
|
||||
writeAllLines(lines)
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
// 4) PARSERS + SERIALIZERS (exact same behavior you had)
|
||||
// --------------------------------------------------------------------------------------------
|
||||
|
||||
private fun empty(): Array<String> = Array(TOTAL_COLUMNS) { "" }
|
||||
|
||||
private fun parseAnimalProfile(row: Array<String>?): AnimalProfile? {
|
||||
row ?: return null
|
||||
val id = row.getOrNull(INDEX_ID)?.takeIf { it.isNotBlank() } ?: return null
|
||||
|
||||
return AnimalProfile(
|
||||
animalId = id,
|
||||
name = row[INDEX_NAME],
|
||||
species = row[INDEX_SPECIES],
|
||||
breed = row[INDEX_BREED],
|
||||
sex = row[INDEX_SEX],
|
||||
weight = row[INDEX_WEIGHT].toIntOrNull() ?: 0,
|
||||
age = row[INDEX_AGE].toIntOrNull() ?: 0,
|
||||
imageUrls = row[INDEX_IMAGES].split(";").filter { it.isNotBlank() },
|
||||
overallRating = row[INDEX_RATING_OVERALL].toIntOrNull()
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseAnimalDetails(row: Array<String>?): AnimalDetails? {
|
||||
row ?: return null
|
||||
val id = row.getOrNull(INDEX_ID)?.takeIf { it.isNotBlank() } ?: return null
|
||||
|
||||
return AnimalDetails(
|
||||
animalId = id,
|
||||
name = row[INDEX_NAME],
|
||||
species = row[INDEX_SPECIES],
|
||||
breed = row[INDEX_BREED],
|
||||
sex = row[INDEX_SEX],
|
||||
weight = row[INDEX_WEIGHT].toIntOrNull() ?: 0,
|
||||
age = row[INDEX_AGE].toIntOrNull() ?: 0,
|
||||
milkYield = row[INDEX_MILK].toIntOrNull() ?: 0,
|
||||
calvingNumber = row[INDEX_CALVING].toIntOrNull() ?: 0,
|
||||
reproductiveStatus = row[INDEX_REPRO],
|
||||
description = row[INDEX_DESC],
|
||||
images = row[INDEX_IMAGES].split(";").filter { it.isNotBlank() },
|
||||
video = row[INDEX_VIDEO]
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseAnimalRating(row: Array<String>?): AnimalRating? {
|
||||
row ?: return null
|
||||
val id = row.getOrNull(INDEX_ID)?.takeIf { it.isNotBlank() } ?: return null
|
||||
|
||||
return AnimalRating(
|
||||
animalId = id,
|
||||
overallRating = row[INDEX_RATING_OVERALL].toIntOrNull() ?: 0,
|
||||
healthRating = row[INDEX_RATING_HEALTH].toIntOrNull() ?: 0,
|
||||
breedRating = row[INDEX_RATING_BREED].toIntOrNull() ?: 0,
|
||||
stature = row[INDEX_RATING_STATURE].toIntOrNull() ?: 0,
|
||||
chestWidth = row[INDEX_RATING_CHEST].toIntOrNull() ?: 0,
|
||||
bodyDepth = row[INDEX_RATING_BODY_DEPTH].toIntOrNull() ?: 0,
|
||||
angularity = row[INDEX_RATING_ANGULARITY].toIntOrNull() ?: 0,
|
||||
rumpAngle = row[INDEX_RATING_RUMP_ANGLE].toIntOrNull() ?: 0,
|
||||
rumpWidth = row[INDEX_RATING_RUMP_WIDTH].toIntOrNull() ?: 0,
|
||||
rearLegSet = row[INDEX_RATING_REAR_LEG_SET].toIntOrNull() ?: 0,
|
||||
rearLegRearView = row[INDEX_RATING_REAR_LEG_REAR].toIntOrNull() ?: 0,
|
||||
footAngle = row[INDEX_RATING_FOOT_ANGLE].toIntOrNull() ?: 0,
|
||||
foreUdderAttachment = row[INDEX_RATING_FORE_UDDER].toIntOrNull() ?: 0,
|
||||
rearUdderHeight = row[INDEX_RATING_REAR_UDDER_HEIGHT].toIntOrNull() ?: 0,
|
||||
centralLigament = row[INDEX_RATING_CENTRAL_LIG].toIntOrNull() ?: 0,
|
||||
udderDepth = row[INDEX_RATING_UDDER_DEPTH].toIntOrNull() ?: 0,
|
||||
frontTeatPosition = row[INDEX_RATING_FRONT_TEAT].toIntOrNull() ?: 0,
|
||||
teatLength = row[INDEX_RATING_TEAT_LEN].toIntOrNull() ?: 0,
|
||||
rearTeatPosition = row[INDEX_RATING_REAR_TEAT].toIntOrNull() ?: 0,
|
||||
locomotion = row[INDEX_RATING_LOCOMOTION].toIntOrNull() ?: 0,
|
||||
bodyConditionScore = row[INDEX_RATING_BCS].toIntOrNull() ?: 0,
|
||||
hockDevelopment = row[INDEX_RATING_HOCK].toIntOrNull() ?: 0,
|
||||
boneStructure = row[INDEX_RATING_BONE].toIntOrNull() ?: 0,
|
||||
rearUdderWidth = row[INDEX_RATING_REAR_UDDER_WIDTH].toIntOrNull() ?: 0,
|
||||
teatThickness = row[INDEX_RATING_TEAT_THICKNESS].toIntOrNull() ?: 0,
|
||||
muscularity = row[INDEX_RATING_MUSCULARITY].toIntOrNull() ?: 0,
|
||||
bodyConditionComments = row[INDEX_RATING_BODY_COND_COMMENTS]
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateProfile(row: Array<String>, p: AnimalProfile): Array<String> {
|
||||
row[INDEX_ID] = p.animalId
|
||||
row[INDEX_NAME] = p.name
|
||||
row[INDEX_SPECIES] = p.species
|
||||
row[INDEX_BREED] = p.breed
|
||||
row[INDEX_SEX] = p.sex
|
||||
row[INDEX_WEIGHT] = p.weight.toString()
|
||||
row[INDEX_AGE] = p.age.toString()
|
||||
row[INDEX_IMAGES] = p.imageUrls.joinToString(";")
|
||||
return row
|
||||
}
|
||||
|
||||
private fun updateDetails(row: Array<String>, d: AnimalDetails): Array<String> {
|
||||
row[INDEX_ID] = d.animalId
|
||||
row[INDEX_NAME] = d.name
|
||||
row[INDEX_SPECIES] = d.species
|
||||
row[INDEX_BREED] = d.breed
|
||||
row[INDEX_SEX] = d.sex
|
||||
row[INDEX_WEIGHT] = d.weight.toString()
|
||||
row[INDEX_AGE] = d.age.toString()
|
||||
row[INDEX_MILK] = d.milkYield.toString()
|
||||
row[INDEX_CALVING] = d.calvingNumber.toString()
|
||||
row[INDEX_REPRO] = d.reproductiveStatus
|
||||
row[INDEX_DESC] = d.description
|
||||
row[INDEX_IMAGES] = d.images.joinToString(";")
|
||||
row[INDEX_VIDEO] = d.video
|
||||
return row
|
||||
}
|
||||
|
||||
private fun updateRating(row: Array<String>, r: AnimalRating): Array<String> {
|
||||
row[INDEX_ID] = r.animalId
|
||||
row[INDEX_RATING_OVERALL] = r.overallRating.toString()
|
||||
row[INDEX_RATING_HEALTH] = r.healthRating.toString()
|
||||
row[INDEX_RATING_BREED] = r.breedRating.toString()
|
||||
row[INDEX_RATING_STATURE] = r.stature.toString()
|
||||
row[INDEX_RATING_CHEST] = r.chestWidth.toString()
|
||||
row[INDEX_RATING_BODY_DEPTH] = r.bodyDepth.toString()
|
||||
row[INDEX_RATING_ANGULARITY] = r.angularity.toString()
|
||||
row[INDEX_RATING_RUMP_ANGLE] = r.rumpAngle.toString()
|
||||
row[INDEX_RATING_RUMP_WIDTH] = r.rumpWidth.toString()
|
||||
row[INDEX_RATING_REAR_LEG_SET] = r.rearLegSet.toString()
|
||||
row[INDEX_RATING_REAR_LEG_REAR] = r.rearLegRearView.toString()
|
||||
row[INDEX_RATING_FOOT_ANGLE] = r.footAngle.toString()
|
||||
row[INDEX_RATING_FORE_UDDER] = r.foreUdderAttachment.toString()
|
||||
row[INDEX_RATING_REAR_UDDER_HEIGHT] = r.rearUdderHeight.toString()
|
||||
row[INDEX_RATING_CENTRAL_LIG] = r.centralLigament.toString()
|
||||
row[INDEX_RATING_UDDER_DEPTH] = r.udderDepth.toString()
|
||||
row[INDEX_RATING_FRONT_TEAT] = r.frontTeatPosition.toString()
|
||||
row[INDEX_RATING_TEAT_LEN] = r.teatLength.toString()
|
||||
row[INDEX_RATING_REAR_TEAT] = r.rearTeatPosition.toString()
|
||||
row[INDEX_RATING_LOCOMOTION] = r.locomotion.toString()
|
||||
row[INDEX_RATING_BCS] = r.bodyConditionScore.toString()
|
||||
row[INDEX_RATING_HOCK] = r.hockDevelopment.toString()
|
||||
row[INDEX_RATING_BONE] = r.boneStructure.toString()
|
||||
row[INDEX_RATING_REAR_UDDER_WIDTH] = r.rearUdderWidth.toString()
|
||||
row[INDEX_RATING_TEAT_THICKNESS] = r.teatThickness.toString()
|
||||
row[INDEX_RATING_MUSCULARITY] = r.muscularity.toString()
|
||||
row[INDEX_RATING_BODY_COND_COMMENTS] = r.bodyConditionComments
|
||||
return row
|
||||
}
|
||||
|
||||
private fun createProfile(p: AnimalProfile) = updateProfile(empty(), p)
|
||||
private fun createDetails(d: AnimalDetails) = updateDetails(empty(), d)
|
||||
private fun createRating(r: AnimalRating) = updateRating(empty(), r)
|
||||
|
||||
companion object {
|
||||
// Same columns as before
|
||||
const val INDEX_ID = 0
|
||||
const val INDEX_NAME = 1
|
||||
const val INDEX_SPECIES = 2
|
||||
const val INDEX_BREED = 3
|
||||
const val INDEX_SEX = 4
|
||||
const val INDEX_WEIGHT = 5
|
||||
const val INDEX_AGE = 6
|
||||
const val INDEX_MILK = 7
|
||||
const val INDEX_CALVING = 8
|
||||
const val INDEX_REPRO = 9
|
||||
const val INDEX_DESC = 10
|
||||
const val INDEX_IMAGES = 11
|
||||
const val INDEX_VIDEO = 12
|
||||
const val INDEX_RATING_OVERALL = 13
|
||||
const val INDEX_RATING_HEALTH = 14
|
||||
const val INDEX_RATING_BREED = 15
|
||||
const val INDEX_RATING_STATURE = 16
|
||||
const val INDEX_RATING_CHEST = 17
|
||||
const val INDEX_RATING_BODY_DEPTH = 18
|
||||
const val INDEX_RATING_ANGULARITY = 19
|
||||
const val INDEX_RATING_RUMP_ANGLE = 20
|
||||
const val INDEX_RATING_RUMP_WIDTH = 21
|
||||
const val INDEX_RATING_REAR_LEG_SET = 22
|
||||
const val INDEX_RATING_REAR_LEG_REAR = 23
|
||||
const val INDEX_RATING_FOOT_ANGLE = 24
|
||||
const val INDEX_RATING_FORE_UDDER = 25
|
||||
const val INDEX_RATING_REAR_UDDER_HEIGHT = 26
|
||||
const val INDEX_RATING_CENTRAL_LIG = 27
|
||||
const val INDEX_RATING_UDDER_DEPTH = 28
|
||||
const val INDEX_RATING_FRONT_TEAT = 29
|
||||
const val INDEX_RATING_TEAT_LEN = 30
|
||||
const val INDEX_RATING_REAR_TEAT = 31
|
||||
const val INDEX_RATING_LOCOMOTION = 32
|
||||
const val INDEX_RATING_BCS = 33
|
||||
const val INDEX_RATING_HOCK = 34
|
||||
const val INDEX_RATING_BONE = 35
|
||||
const val INDEX_RATING_REAR_UDDER_WIDTH = 36
|
||||
const val INDEX_RATING_TEAT_THICKNESS = 37
|
||||
const val INDEX_RATING_MUSCULARITY = 38
|
||||
const val INDEX_RATING_BODY_COND_COMMENTS = 39
|
||||
|
||||
const val TOTAL_COLUMNS = 40
|
||||
|
||||
val HEADER = arrayOf(
|
||||
"ID", "Name", "Species", "Breed", "Sex", "Weight", "Age", "MilkYield",
|
||||
"CalvingNum", "ReproStatus", "Description", "Images", "Video",
|
||||
"OverallRating", "HealthRating", "BreedRating", "Stature", "ChestWidth",
|
||||
"BodyDepth", "Angularity", "RumpAngle", "RumpWidth", "RearLegSet",
|
||||
"RearLegRearView", "FootAngle", "ForeUdderAttachment", "RearUdderHeight",
|
||||
"CentralLigament", "UdderDepth", "FrontTeatPosition", "TeatLength",
|
||||
"RearTeatPosition", "Locomotion", "BodyConditionScore", "HockDevelopment",
|
||||
"BoneStructure", "RearUdderWidth", "TeatThickness", "Muscularity",
|
||||
"BodyConditionComments"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.example.livingai.data.local.model
|
||||
|
||||
data class SettingsData(
|
||||
val language: String,
|
||||
val isAutoCaptureOn: Boolean
|
||||
)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.example.livingai.data.manager
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import com.example.livingai.domain.manager.LocalUserManager
|
||||
import com.example.livingai.utils.Constants
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class LocalUserManagerImpl(
|
||||
private val dataStore: DataStore<Preferences>
|
||||
): LocalUserManager {
|
||||
override suspend fun saveAppEntry() {
|
||||
dataStore.edit { settings ->
|
||||
settings[PreferencesKeys.APP_ENTRY] = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun readAppEntry(): Flow<Boolean> {
|
||||
return dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.APP_ENTRY] ?: false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private object PreferencesKeys {
|
||||
val APP_ENTRY = booleanPreferencesKey(name = Constants.APP_ENTRY)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.example.livingai.data.ml
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.example.livingai.domain.ml.AIModel
|
||||
|
||||
class AIModelImpl : AIModel {
|
||||
override fun deriveInference(bitmap: Bitmap): String {
|
||||
// Placeholder for actual inference logic
|
||||
return "Inference Result"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.example.livingai.data.repository
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.example.livingai.data.local.model.SettingsData
|
||||
import com.example.livingai.domain.repository.SettingsRepository
|
||||
import com.example.livingai.utils.Constants.APP_ENTRY
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.io.IOException
|
||||
|
||||
class SettingsRepositoryImpl(private val dataStore: DataStore<Preferences>) : SettingsRepository {
|
||||
|
||||
private object PreferencesKeys {
|
||||
val LANGUAGE = stringPreferencesKey("language")
|
||||
val IS_AUTO_CAPTURE_ON = booleanPreferencesKey("is_auto_capture_on")
|
||||
}
|
||||
|
||||
override fun getSettings(): Flow<SettingsData> {
|
||||
return dataStore.data.catch {
|
||||
if (it is IOException) {
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}.map {
|
||||
val language = it[PreferencesKeys.LANGUAGE] ?: "en"
|
||||
val isAutoCaptureOn = it[PreferencesKeys.IS_AUTO_CAPTURE_ON] ?: false
|
||||
SettingsData(language, isAutoCaptureOn)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveSettings(settings: SettingsData) {
|
||||
dataStore.edit {
|
||||
it[PreferencesKeys.LANGUAGE] = settings.language
|
||||
it[PreferencesKeys.IS_AUTO_CAPTURE_ON] = settings.isAutoCaptureOn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.example.livingai.data.repository.business
|
||||
|
||||
import com.example.livingai.domain.model.AnimalDetails
|
||||
import com.example.livingai.domain.repository.business.AnimalDetailsRepository
|
||||
import com.example.livingai.domain.repository.business.DataSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class AnimalDetailsRepositoryImpl(
|
||||
private val dataSource: DataSource
|
||||
) : AnimalDetailsRepository {
|
||||
override fun getAnimalDetails(id: String): Flow<AnimalDetails?> {
|
||||
return dataSource.getAnimalDetails(id)
|
||||
}
|
||||
|
||||
override suspend fun saveAnimalDetails(animalDetails: AnimalDetails) {
|
||||
dataSource.setAnimalDetails(animalDetails)
|
||||
}
|
||||
|
||||
override suspend fun deleteAnimalDetails(id: String) {
|
||||
// Currently only full profile deletion is exposed by DataSource as per request
|
||||
// but we can call that if needed, or just leave it as no-op if details deletion is specific
|
||||
// Assuming for now it deletes the profile or we might need to add specific delete to DataSource
|
||||
// But the prompt said "DataSource should have get, set and delete... get and delete based on a string which will be id"
|
||||
// And "deleteAnimalProfile - takes an Id deletes that animals complete profile"
|
||||
// So strictly for details, maybe we don't have a specific delete or we just don't impl it yet.
|
||||
// However, to satisfy the interface:
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.example.livingai.data.repository.business
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.example.livingai.domain.model.AnimalProfile
|
||||
import com.example.livingai.domain.repository.business.AnimalProfileRepository
|
||||
import com.example.livingai.domain.repository.business.DataSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class AnimalProfileRepositoryImpl(
|
||||
private val dataSource: DataSource
|
||||
) : AnimalProfileRepository {
|
||||
override fun getAnimalProfiles(): Flow<PagingData<AnimalProfile>> {
|
||||
return dataSource.getAnimalProfiles()
|
||||
}
|
||||
|
||||
override suspend fun saveAnimalProfile(animalProfile: AnimalProfile) {
|
||||
dataSource.setAnimalProfile(animalProfile)
|
||||
}
|
||||
|
||||
override suspend fun deleteAnimalProfile(id: String) {
|
||||
dataSource.deleteAnimalProfile(id)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.example.livingai.data.repository.business
|
||||
|
||||
import com.example.livingai.domain.model.AnimalRating
|
||||
import com.example.livingai.domain.repository.business.AnimalRatingRepository
|
||||
import com.example.livingai.domain.repository.business.DataSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class AnimalRatingRepositoryImpl(
|
||||
private val dataSource: DataSource
|
||||
) : AnimalRatingRepository {
|
||||
override fun getAnimalRating(id: String): Flow<AnimalRating?> {
|
||||
return dataSource.getAnimalRatings(id)
|
||||
}
|
||||
|
||||
override suspend fun saveAnimalRating(animalRating: AnimalRating) {
|
||||
dataSource.setAnimalRatings(animalRating)
|
||||
}
|
||||
|
||||
override suspend fun deleteAnimalRating(id: String) {
|
||||
// Same as details, placeholder for now as only full profile delete is requested in DataSource
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.example.livingai.data.repository.media
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Matrix
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import androidx.camera.core.ImageProxy
|
||||
import com.example.livingai.domain.ml.AIModel
|
||||
import com.example.livingai.domain.repository.CameraRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class CameraRepositoryImpl(
|
||||
private val aiModel: AIModel,
|
||||
private val context: Context
|
||||
) : CameraRepository {
|
||||
|
||||
override suspend fun captureImage(imageProxy: ImageProxy): Bitmap = withContext(Dispatchers.IO) {
|
||||
val rotationDegrees = imageProxy.imageInfo.rotationDegrees
|
||||
val bitmap = imageProxy.toBitmap()
|
||||
imageProxy.close()
|
||||
|
||||
// Rotate bitmap if needed
|
||||
if (rotationDegrees != 0) {
|
||||
val matrix = Matrix().apply { postRotate(rotationDegrees.toFloat()) }
|
||||
Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
||||
} else {
|
||||
bitmap
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun processFrame(bitmap: Bitmap): String = withContext(Dispatchers.Default) {
|
||||
aiModel.deriveInference(bitmap)
|
||||
}
|
||||
|
||||
override suspend fun saveImage(bitmap: Bitmap, animalId: String, orientation: String?): String = withContext(Dispatchers.IO) {
|
||||
val orientationSuffix = orientation?.let { "_$it" } ?: ""
|
||||
val filename = "${animalId}${orientationSuffix}.jpg"
|
||||
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.Images.Media.DISPLAY_NAME, filename)
|
||||
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/LivingAI/Media/$animalId")
|
||||
put(MediaStore.Images.Media.IS_PENDING, 1)
|
||||
}
|
||||
}
|
||||
|
||||
val resolver = context.contentResolver
|
||||
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||
?: throw RuntimeException("Failed to create image record")
|
||||
|
||||
try {
|
||||
resolver.openOutputStream(uri)?.use { out ->
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||
contentValues.clear()
|
||||
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
|
||||
resolver.update(uri, contentValues, null, null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
resolver.delete(uri, null, null)
|
||||
throw e
|
||||
}
|
||||
|
||||
uri.toString()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.example.livingai.data.repository.media
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.example.livingai.domain.ml.AIModel
|
||||
import com.example.livingai.domain.repository.VideoRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class VideoRepositoryImpl(private val aiModel: AIModel) : VideoRepository {
|
||||
|
||||
private var isRecording = false
|
||||
|
||||
override fun startRecording(onRecordingStarted: () -> Unit) {
|
||||
isRecording = true
|
||||
// Logic to start recording video if needed
|
||||
onRecordingStarted()
|
||||
}
|
||||
|
||||
override fun stopRecording() {
|
||||
isRecording = false
|
||||
// Logic to stop recording
|
||||
}
|
||||
|
||||
override suspend fun processFrame(bitmap: Bitmap): String = withContext(Dispatchers.Default) {
|
||||
if (isRecording) {
|
||||
aiModel.deriveInference(bitmap)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package com.example.livingai.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.example.livingai.data.local.CSVDataSource
|
||||
import com.example.livingai.data.manager.LocalUserManagerImpl
|
||||
import com.example.livingai.data.ml.AIModelImpl
|
||||
import com.example.livingai.data.repository.SettingsRepositoryImpl
|
||||
import com.example.livingai.data.repository.business.AnimalDetailsRepositoryImpl
|
||||
import com.example.livingai.data.repository.business.AnimalProfileRepositoryImpl
|
||||
import com.example.livingai.data.repository.business.AnimalRatingRepositoryImpl
|
||||
import com.example.livingai.data.repository.media.CameraRepositoryImpl
|
||||
import com.example.livingai.data.repository.media.VideoRepositoryImpl
|
||||
import com.example.livingai.domain.manager.LocalUserManager
|
||||
import com.example.livingai.domain.ml.AIModel
|
||||
import com.example.livingai.domain.repository.CameraRepository
|
||||
import com.example.livingai.domain.repository.SettingsRepository
|
||||
import com.example.livingai.domain.repository.VideoRepository
|
||||
import com.example.livingai.domain.repository.business.AnimalDetailsRepository
|
||||
import com.example.livingai.domain.repository.business.AnimalProfileRepository
|
||||
import com.example.livingai.domain.repository.business.AnimalRatingRepository
|
||||
import com.example.livingai.domain.repository.business.DataSource
|
||||
import com.example.livingai.domain.usecases.AppEntry.AppEntryUseCases
|
||||
import com.example.livingai.domain.usecases.DeleteAnimalProfile
|
||||
import com.example.livingai.domain.usecases.GetAnimalDetails
|
||||
import com.example.livingai.domain.usecases.GetAnimalProfiles
|
||||
import com.example.livingai.domain.usecases.GetAnimalRatings
|
||||
import com.example.livingai.domain.usecases.ProfileEntry.ProfileEntryUseCase
|
||||
import com.example.livingai.domain.usecases.ProfileListing.ProfileListingUseCase
|
||||
import com.example.livingai.domain.usecases.ReadAppEntry
|
||||
import com.example.livingai.domain.usecases.SaveAppEntry
|
||||
import com.example.livingai.domain.usecases.SetAnimalDetails
|
||||
import com.example.livingai.domain.usecases.SetAnimalRatings
|
||||
import com.example.livingai.pages.addprofile.AddProfileViewModel
|
||||
import com.example.livingai.pages.camera.CameraViewModel
|
||||
import com.example.livingai.pages.camera.VideoViewModel
|
||||
import com.example.livingai.pages.home.HomeViewModel
|
||||
import com.example.livingai.pages.listings.ListingsViewModel
|
||||
import com.example.livingai.pages.onboarding.OnBoardingViewModel
|
||||
import com.example.livingai.pages.ratings.RatingViewModel
|
||||
import com.example.livingai.pages.settings.SettingsViewModel
|
||||
import com.example.livingai.utils.Constants
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.module.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = Constants.USER_SETTINGS)
|
||||
|
||||
val appModule = module {
|
||||
single<LocalUserManager> { LocalUserManagerImpl(get()) }
|
||||
single<DataStore<Preferences>> { androidContext().dataStore }
|
||||
|
||||
single {
|
||||
AppEntryUseCases(
|
||||
readAppEntry = ReadAppEntry(get()),
|
||||
saveAppEntry = SaveAppEntry(get())
|
||||
)
|
||||
}
|
||||
|
||||
// Data Source
|
||||
single<DataSource> {
|
||||
CSVDataSource(
|
||||
context = androidContext(),
|
||||
fileName = Constants.ANIMAL_DATA_FILENAME
|
||||
)
|
||||
}
|
||||
|
||||
// ML Model
|
||||
single<AIModel> { AIModelImpl() }
|
||||
|
||||
// Repositories
|
||||
single<AnimalProfileRepository> { AnimalProfileRepositoryImpl(get()) }
|
||||
single<AnimalDetailsRepository> { AnimalDetailsRepositoryImpl(get()) }
|
||||
single<AnimalRatingRepository> { AnimalRatingRepositoryImpl(get()) }
|
||||
single<SettingsRepository> { SettingsRepositoryImpl(get()) }
|
||||
single<CameraRepository> { CameraRepositoryImpl(get(), androidContext()) }
|
||||
single<VideoRepository> { VideoRepositoryImpl(get()) }
|
||||
|
||||
// Use Cases
|
||||
single { GetAnimalProfiles(get()) }
|
||||
single { GetAnimalDetails(get()) }
|
||||
single { GetAnimalRatings(get()) }
|
||||
single { SetAnimalDetails(get()) }
|
||||
single { SetAnimalRatings(get()) }
|
||||
single { DeleteAnimalProfile(get()) }
|
||||
|
||||
//Use Cases
|
||||
single {
|
||||
ProfileEntryUseCase(
|
||||
getAnimalDetails = GetAnimalDetails(get()),
|
||||
setAnimalDetails = SetAnimalDetails(get())
|
||||
)
|
||||
}
|
||||
|
||||
single {
|
||||
ProfileListingUseCase(
|
||||
getAnimalProfiles = GetAnimalProfiles(get()),
|
||||
deleteAnimalProfile = DeleteAnimalProfile(get())
|
||||
)
|
||||
}
|
||||
|
||||
// ViewModels
|
||||
viewModel { HomeViewModel(get()) }
|
||||
viewModel { OnBoardingViewModel(get(), get()) }
|
||||
viewModel { AddProfileViewModel(get()) }
|
||||
viewModel { ListingsViewModel(get()) }
|
||||
viewModel { SettingsViewModel(get()) }
|
||||
viewModel { RatingViewModel(get(), get(), get(), get()) }
|
||||
viewModel { CameraViewModel(get()) }
|
||||
viewModel { VideoViewModel() }
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.example.livingai.domain.manager
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface LocalUserManager {
|
||||
suspend fun saveAppEntry()
|
||||
fun readAppEntry(): Flow<Boolean>
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.example.livingai.domain.ml
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
interface AIModel {
|
||||
fun deriveInference(bitmap: Bitmap): String
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.example.livingai.domain.model
|
||||
|
||||
data class AnimalDetails(
|
||||
val animalId: String,
|
||||
val name: String,
|
||||
val species: String,
|
||||
val breed: String,
|
||||
val sex: String,
|
||||
val weight: Int,
|
||||
val age: Int,
|
||||
val milkYield: Int,
|
||||
val calvingNumber: Int,
|
||||
val reproductiveStatus: String,
|
||||
val description: String,
|
||||
val images: List<String>,
|
||||
val video: String
|
||||
)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.example.livingai.domain.model
|
||||
|
||||
data class AnimalProfile(
|
||||
val animalId: String,
|
||||
val name: String,
|
||||
val species: String,
|
||||
val breed: String,
|
||||
val sex: String,
|
||||
val weight: Int,
|
||||
val age: Int,
|
||||
val overallRating: Int? = null,
|
||||
val imageUrls: List<String>
|
||||
)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.example.livingai.domain.model
|
||||
|
||||
data class AnimalRating(
|
||||
val animalId: String,
|
||||
val overallRating: Int,
|
||||
val healthRating: Int,
|
||||
val breedRating: Int,
|
||||
val stature: Int,
|
||||
val chestWidth: Int,
|
||||
val bodyDepth: Int,
|
||||
val angularity: Int,
|
||||
val rumpAngle: Int,
|
||||
val rumpWidth: Int,
|
||||
val rearLegSet: Int,
|
||||
val rearLegRearView: Int,
|
||||
val footAngle: Int,
|
||||
val foreUdderAttachment: Int,
|
||||
val rearUdderHeight: Int,
|
||||
val centralLigament: Int,
|
||||
val udderDepth: Int,
|
||||
val frontTeatPosition: Int,
|
||||
val teatLength: Int,
|
||||
val rearTeatPosition: Int,
|
||||
val locomotion: Int,
|
||||
val bodyConditionScore: Int,
|
||||
val hockDevelopment: Int,
|
||||
val boneStructure: Int,
|
||||
val rearUdderWidth: Int,
|
||||
val teatThickness: Int,
|
||||
val muscularity: Int,
|
||||
val bodyConditionComments: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.example.livingai.domain.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SettingsData(
|
||||
val language: String = "en",
|
||||
val isAutoCaptureOn: Boolean = false
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.example.livingai.domain.repository
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.camera.core.ImageProxy
|
||||
|
||||
interface CameraRepository {
|
||||
suspend fun captureImage(imageProxy: ImageProxy): Bitmap
|
||||
suspend fun processFrame(bitmap: Bitmap): String
|
||||
suspend fun saveImage(bitmap: Bitmap, animalId: String, orientation: String?): String
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.example.livingai.domain.repository
|
||||
|
||||
import com.example.livingai.data.local.model.SettingsData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SettingsRepository {
|
||||
fun getSettings(): Flow<SettingsData>
|
||||
suspend fun saveSettings(settings: SettingsData)
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.example.livingai.domain.repository
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.camera.video.Recording
|
||||
|
||||
interface VideoRepository {
|
||||
fun startRecording(onRecordingStarted: () -> Unit)
|
||||
fun stopRecording()
|
||||
suspend fun processFrame(bitmap: Bitmap): String
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.example.livingai.domain.repository.business
|
||||
|
||||
import com.example.livingai.domain.model.AnimalDetails
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AnimalDetailsRepository {
|
||||
fun getAnimalDetails(id: String): Flow<AnimalDetails?>
|
||||
|
||||
suspend fun saveAnimalDetails(animalDetails: AnimalDetails)
|
||||
|
||||
suspend fun deleteAnimalDetails(id: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.example.livingai.domain.repository.business
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.example.livingai.domain.model.AnimalProfile
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AnimalProfileRepository {
|
||||
fun getAnimalProfiles(): Flow<PagingData<AnimalProfile>>
|
||||
|
||||
suspend fun saveAnimalProfile(animalProfile: AnimalProfile)
|
||||
|
||||
suspend fun deleteAnimalProfile(id: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.example.livingai.domain.repository.business
|
||||
|
||||
import com.example.livingai.domain.model.AnimalRating
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AnimalRatingRepository {
|
||||
fun getAnimalRating(id: String): Flow<AnimalRating?>
|
||||
|
||||
suspend fun saveAnimalRating(animalRating: AnimalRating)
|
||||
|
||||
suspend fun deleteAnimalRating(id: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.example.livingai.domain.repository.business
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.example.livingai.domain.model.AnimalDetails
|
||||
import com.example.livingai.domain.model.AnimalProfile
|
||||
import com.example.livingai.domain.model.AnimalRating
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface DataSource {
|
||||
fun getAnimalProfiles(): Flow<PagingData<AnimalProfile>>
|
||||
fun getAnimalDetails(animalId: String): Flow<AnimalDetails?>
|
||||
fun getAnimalRatings(animalId: String): Flow<AnimalRating?>
|
||||
|
||||
suspend fun setAnimalProfile(animalProfile: AnimalProfile)
|
||||
suspend fun setAnimalDetails(animalDetails: AnimalDetails)
|
||||
suspend fun setAnimalRatings(animalRating: AnimalRating)
|
||||
suspend fun deleteAnimalProfile(animalId: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.example.livingai.domain.usecases.AppEntry
|
||||
|
||||
import com.example.livingai.domain.usecases.ReadAppEntry
|
||||
import com.example.livingai.domain.usecases.SaveAppEntry
|
||||
|
||||
data class AppEntryUseCases(
|
||||
val readAppEntry: ReadAppEntry,
|
||||
val saveAppEntry: SaveAppEntry
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.example.livingai.domain.usecases
|
||||
|
||||
import com.example.livingai.domain.repository.business.AnimalProfileRepository
|
||||
|
||||
class DeleteAnimalProfile(
|
||||
private val animalProfileRepository: AnimalProfileRepository
|
||||
) {
|
||||
suspend operator fun invoke(animalId: String) {
|
||||
animalProfileRepository.deleteAnimalProfile(animalId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.example.livingai.domain.usecases
|
||||
|
||||
import com.example.livingai.domain.model.AnimalDetails
|
||||
import com.example.livingai.domain.repository.business.AnimalDetailsRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetAnimalDetails(
|
||||
private val animalDetailsRepository: AnimalDetailsRepository
|
||||
) {
|
||||
operator fun invoke(animalId: String): Flow<AnimalDetails?> {
|
||||
return animalDetailsRepository.getAnimalDetails(animalId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.example.livingai.domain.usecases
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.example.livingai.domain.model.AnimalProfile
|
||||
import com.example.livingai.domain.repository.business.AnimalProfileRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetAnimalProfiles(
|
||||
private val animalProfileRepository: AnimalProfileRepository
|
||||
) {
|
||||
operator fun invoke(): Flow<PagingData<AnimalProfile>> {
|
||||
return animalProfileRepository.getAnimalProfiles()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.example.livingai.domain.usecases
|
||||
|
||||
import com.example.livingai.domain.model.AnimalRating
|
||||
import com.example.livingai.domain.repository.business.AnimalRatingRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetAnimalRatings(
|
||||
private val animalRatingRepository: AnimalRatingRepository
|
||||
) {
|
||||
operator fun invoke(animalId: String): Flow<AnimalRating?> {
|
||||
return animalRatingRepository.getAnimalRating(animalId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.example.livingai.domain.usecases.ProfileEntry
|
||||
|
||||
import com.example.livingai.domain.usecases.GetAnimalDetails
|
||||
import com.example.livingai.domain.usecases.SetAnimalDetails
|
||||
|
||||
data class ProfileEntryUseCase(
|
||||
val getAnimalDetails: GetAnimalDetails,
|
||||
val setAnimalDetails: SetAnimalDetails,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.example.livingai.domain.usecases.ProfileListing
|
||||
|
||||
import com.example.livingai.domain.usecases.DeleteAnimalProfile
|
||||
import com.example.livingai.domain.usecases.GetAnimalProfiles
|
||||
|
||||
data class ProfileListingUseCase(
|
||||
val getAnimalProfiles: GetAnimalProfiles,
|
||||
val deleteAnimalProfile: DeleteAnimalProfile
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.example.livingai.domain.usecases.ProfilesEntry
|
||||
|
||||
import com.example.livingai.domain.usecases.DeleteAnimalProfile
|
||||
import com.example.livingai.domain.usecases.GetAnimalProfiles
|
||||
|
||||
data class ProfilesEntryUseCases(
|
||||
val getAnimalProfiles: GetAnimalProfiles,
|
||||
val deleteAnimalProfile: DeleteAnimalProfile
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.example.livingai.domain.usecases
|
||||
|
||||
import com.example.livingai.domain.manager.LocalUserManager
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class ReadAppEntry(
|
||||
private val localUserManager: LocalUserManager
|
||||
) {
|
||||
operator fun invoke(): Flow<Boolean> {
|
||||
return localUserManager.readAppEntry()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.example.livingai.domain.usecases
|
||||
|
||||
import com.example.livingai.domain.manager.LocalUserManager
|
||||
|
||||
class SaveAppEntry(
|
||||
private val localUserManager: LocalUserManager
|
||||
) {
|
||||
suspend operator fun invoke() {
|
||||
localUserManager.saveAppEntry()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.example.livingai.domain.usecases
|
||||
|
||||
import com.example.livingai.domain.model.AnimalDetails
|
||||
import com.example.livingai.domain.repository.business.AnimalDetailsRepository
|
||||
|
||||
class SetAnimalDetails(
|
||||
private val animalDetailsRepository: AnimalDetailsRepository
|
||||
) {
|
||||
suspend operator fun invoke(animalDetails: AnimalDetails) {
|
||||
animalDetailsRepository.saveAnimalDetails(animalDetails)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.example.livingai.domain.usecases
|
||||
|
||||
import com.example.livingai.domain.model.AnimalRating
|
||||
import com.example.livingai.domain.repository.business.AnimalRatingRepository
|
||||
|
||||
class SetAnimalRatings(
|
||||
private val animalRatingRepository: AnimalRatingRepository
|
||||
) {
|
||||
suspend operator fun invoke(animalRating: AnimalRating) {
|
||||
animalRatingRepository.saveAnimalRating(animalRating)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
package com.example.livingai.pages.addprofile
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.navigation.NavController
|
||||
import com.example.livingai.R
|
||||
import com.example.livingai.pages.commons.Dimentions
|
||||
import com.example.livingai.pages.components.CommonScaffold
|
||||
import com.example.livingai.pages.components.ImageThumbnailButton
|
||||
import com.example.livingai.pages.components.LabeledDropdown
|
||||
import com.example.livingai.pages.components.LabeledTextField
|
||||
import com.example.livingai.pages.components.RadioGroup
|
||||
import com.example.livingai.pages.components.VideoThumbnailButton
|
||||
import com.example.livingai.utils.Constants
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@Composable
|
||||
fun AddProfileScreen(
|
||||
navController: NavController,
|
||||
viewModel: AddProfileViewModel,
|
||||
onSave: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
onTakePhoto: (String) -> Unit,
|
||||
onTakeVideo: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val speciesList = stringArrayResource(id = R.array.species_list).toList()
|
||||
val breedList = stringArrayResource(id = R.array.cow_breed_list).toList()
|
||||
|
||||
val reproList = listOf(
|
||||
stringResource(R.string.option_pregnant),
|
||||
stringResource(R.string.option_calved),
|
||||
stringResource(R.string.option_none)
|
||||
)
|
||||
|
||||
val silhouette = Constants.silhouetteList.associate { item ->
|
||||
val resId = context.resources.getIdentifier("label_${item}", "string", context.packageName)
|
||||
|
||||
item to if (resId != 0) resId else R.string.default_orientation_label
|
||||
}
|
||||
|
||||
// Use ViewModel state
|
||||
var species by viewModel.species
|
||||
var breed by viewModel.breed
|
||||
var age by viewModel.age
|
||||
var milkYield by viewModel.milkYield
|
||||
var calvingNumber by viewModel.calvingNumber
|
||||
var reproductiveStatus by viewModel.reproductiveStatus
|
||||
var description by viewModel.description
|
||||
|
||||
val photos = viewModel.photos
|
||||
val videoUri by viewModel.videoUri
|
||||
|
||||
CommonScaffold(
|
||||
navController = navController,
|
||||
title = stringResource(id = R.string.top_bar_add_profile)
|
||||
) { innerPadding ->
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimentions.SMALL_PADDING_INPUT),
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimentions.SMALL_PADDING_IMAGE),
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.padding(Dimentions.SMALL_PADDING_TEXT)
|
||||
) {
|
||||
item(span = { GridItemSpan(2) }) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(Dimentions.SMALL_PADDING_INPUT)
|
||||
) {
|
||||
LabeledDropdown(
|
||||
labelRes = R.string.label_species,
|
||||
options = speciesList,
|
||||
selected = species,
|
||||
onSelected = { species = it }
|
||||
)
|
||||
|
||||
LabeledDropdown(
|
||||
labelRes = R.string.label_breed,
|
||||
options = breedList,
|
||||
selected = breed,
|
||||
onSelected = { breed = it }
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimentions.SMALL_PADDING_INPUT),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
LabeledTextField(
|
||||
labelRes = R.string.label_age,
|
||||
value = age,
|
||||
modifier = Modifier.weight(1f),
|
||||
onValueChange = { age = it },
|
||||
keyboardType = KeyboardType.Number
|
||||
)
|
||||
|
||||
LabeledTextField(
|
||||
labelRes = R.string.label_milk_yield,
|
||||
value = milkYield,
|
||||
modifier = Modifier.weight(1f),
|
||||
onValueChange = { milkYield = it },
|
||||
keyboardType = KeyboardType.Number
|
||||
)
|
||||
}
|
||||
|
||||
LabeledTextField(
|
||||
labelRes = R.string.label_calving_number,
|
||||
value = calvingNumber,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onValueChange = { calvingNumber = it },
|
||||
keyboardType = KeyboardType.Number
|
||||
)
|
||||
|
||||
RadioGroup(
|
||||
titleRes = R.string.label_reproductive_status,
|
||||
options = reproList,
|
||||
selected = reproductiveStatus,
|
||||
onSelected = { reproductiveStatus = it }
|
||||
)
|
||||
|
||||
LabeledTextField(
|
||||
labelRes = R.string.label_description,
|
||||
value = description,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
onValueChange = { description = it },
|
||||
keyboardType = KeyboardType.Text
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.label_upload_media),
|
||||
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(vertical = Dimentions.SMALL_PADDING_TEXT)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(silhouette.entries.toList()) { (key, value) ->
|
||||
ImageThumbnailButton(
|
||||
image = photos[key],
|
||||
onClick = { onTakePhoto(key) },
|
||||
labelRes = value
|
||||
)
|
||||
}
|
||||
|
||||
// Video Button
|
||||
item {
|
||||
VideoThumbnailButton(
|
||||
videoSource = videoUri, // Removed videoThumbnail fallback as it's not in VM
|
||||
onClick = onTakeVideo
|
||||
)
|
||||
}
|
||||
|
||||
// Save and Cancel Buttons
|
||||
item(span = { GridItemSpan(2) }) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = Dimentions.MEDIUM_PADDING_BUTTON),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onCancel,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = Dimentions.SMALL_PADDING_BUTTON)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.btn_cancel))
|
||||
}
|
||||
Button(
|
||||
onClick = onSave,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = Dimentions.SMALL_PADDING_BUTTON)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.btn_save_profile))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item(span = { GridItemSpan(2) }) {
|
||||
Spacer(modifier = Modifier.height(Dimentions.LARGE_PADDING))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package com.example.livingai.pages.addprofile
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.livingai.domain.model.AnimalDetails
|
||||
import com.example.livingai.domain.usecases.ProfileEntry.ProfileEntryUseCase
|
||||
import com.example.livingai.utils.IdGenerator
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AddProfileViewModel(
|
||||
private val profileEntryUseCase: ProfileEntryUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val _animalDetails = mutableStateOf<AnimalDetails?>(null)
|
||||
val animalDetails: State<AnimalDetails?> = _animalDetails
|
||||
|
||||
private val _currentAnimalId = mutableStateOf<String?>(null)
|
||||
val currentAnimalId: State<String?> = _currentAnimalId
|
||||
|
||||
// UI State
|
||||
var species = mutableStateOf<String?>(null)
|
||||
var breed = mutableStateOf<String?>(null)
|
||||
var age = mutableStateOf("")
|
||||
var milkYield = mutableStateOf("")
|
||||
var calvingNumber = mutableStateOf("")
|
||||
var reproductiveStatus = mutableStateOf<String?>(null)
|
||||
var description = mutableStateOf("")
|
||||
|
||||
// State for photos and video
|
||||
val photos = mutableStateMapOf<String, String>()
|
||||
private val _videoUri = mutableStateOf<String?>(null)
|
||||
val videoUri: State<String?> = _videoUri
|
||||
|
||||
fun loadAnimalDetails(animalId: String?) {
|
||||
if (animalId == null) {
|
||||
val newId = IdGenerator.generateAnimalId()
|
||||
_currentAnimalId.value = newId
|
||||
_animalDetails.value = null
|
||||
|
||||
// Reset UI State
|
||||
species.value = null
|
||||
breed.value = null
|
||||
age.value = ""
|
||||
milkYield.value = ""
|
||||
calvingNumber.value = ""
|
||||
reproductiveStatus.value = null
|
||||
description.value = ""
|
||||
|
||||
photos.clear()
|
||||
_videoUri.value = null
|
||||
} else {
|
||||
_currentAnimalId.value = animalId
|
||||
profileEntryUseCase.getAnimalDetails(animalId).onEach { details ->
|
||||
if (details != null) {
|
||||
_animalDetails.value = details
|
||||
|
||||
// Populate UI State
|
||||
species.value = details.species.ifBlank { null }
|
||||
breed.value = details.breed.ifBlank { null }
|
||||
age.value = if (details.age == 0) "" else details.age.toString()
|
||||
milkYield.value = if (details.milkYield == 0) "" else details.milkYield.toString()
|
||||
calvingNumber.value = if (details.calvingNumber == 0) "" else details.calvingNumber.toString()
|
||||
reproductiveStatus.value = details.reproductiveStatus.ifBlank { null }
|
||||
description.value = details.description
|
||||
|
||||
// Populate photos
|
||||
photos.clear()
|
||||
details.images.forEach { path ->
|
||||
// path: .../{id}_{orientation}.jpg
|
||||
val filename = path.substringAfterLast('/')
|
||||
val nameWithoutExt = filename.substringBeforeLast('.')
|
||||
val parts = nameWithoutExt.split('_')
|
||||
if (parts.size >= 2) {
|
||||
val orientation = parts.last()
|
||||
photos[orientation] = path
|
||||
}
|
||||
}
|
||||
_videoUri.value = details.video.ifBlank { null }
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
}
|
||||
|
||||
fun addPhoto(orientation: String, uri: String) {
|
||||
photos[orientation] = uri
|
||||
}
|
||||
|
||||
fun setVideo(uri: String) {
|
||||
_videoUri.value = uri
|
||||
}
|
||||
|
||||
fun saveAnimalDetails() {
|
||||
val id = _currentAnimalId.value ?: IdGenerator.generateAnimalId().also { _currentAnimalId.value = it }
|
||||
|
||||
val details = AnimalDetails(
|
||||
animalId = id,
|
||||
species = species.value ?: "",
|
||||
breed = breed.value ?: "",
|
||||
age = age.value.toIntOrNull() ?: 0,
|
||||
milkYield = milkYield.value.toIntOrNull() ?: 0,
|
||||
calvingNumber = calvingNumber.value.toIntOrNull() ?: 0,
|
||||
reproductiveStatus = reproductiveStatus.value ?: "",
|
||||
description = description.value,
|
||||
images = photos.values.toList(),
|
||||
video = _videoUri.value ?: "",
|
||||
name = "", sex = "", weight = 0
|
||||
)
|
||||
|
||||
viewModelScope.launch {
|
||||
profileEntryUseCase.setAnimalDetails(details)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
package com.example.livingai.pages.camera
|
||||
|
||||
import androidx.camera.core.ImageCapture
|
||||
import androidx.camera.core.ImageCaptureException
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.camera.view.LifecycleCameraController
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Camera
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.navigation.NavController
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import com.example.livingai.pages.components.CameraPreview
|
||||
import com.example.livingai.pages.components.PermissionWrapper
|
||||
import com.example.livingai.pages.navigation.Route
|
||||
|
||||
@Composable
|
||||
fun CameraScreen(
|
||||
viewModel: CameraViewModel = koinViewModel(),
|
||||
navController: NavController,
|
||||
orientation: String? = null,
|
||||
animalId: String
|
||||
) {
|
||||
PermissionWrapper {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val context = LocalContext.current
|
||||
|
||||
val controller = remember {
|
||||
LifecycleCameraController(context).apply {
|
||||
setEnabledUseCases(LifecycleCameraController.IMAGE_ANALYSIS or LifecycleCameraController.IMAGE_CAPTURE)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(animalId, orientation) {
|
||||
viewModel.onEvent(CameraEvent.SetContext(animalId, orientation))
|
||||
}
|
||||
|
||||
LaunchedEffect(state.capturedImageUri) {
|
||||
state.capturedImageUri?.let {
|
||||
navController.navigate(
|
||||
Route.ViewImageScreen(
|
||||
imageUri = it.toString(),
|
||||
shouldAllowRetake = true,
|
||||
showAccept = true,
|
||||
orientation = orientation,
|
||||
animalId = animalId
|
||||
)
|
||||
)
|
||||
viewModel.onEvent(CameraEvent.ClearCapturedImage) // Clear after navigation
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
if (!state.isAutoCaptureOn) {
|
||||
FloatingActionButton(onClick = {
|
||||
val executor = ContextCompat.getMainExecutor(context)
|
||||
controller.takePicture(
|
||||
executor,
|
||||
object : ImageCapture.OnImageCapturedCallback() {
|
||||
override fun onCaptureSuccess(image: ImageProxy) {
|
||||
viewModel.onEvent(CameraEvent.ImageCaptured(image))
|
||||
}
|
||||
|
||||
override fun onError(exception: ImageCaptureException) {
|
||||
// Handle error
|
||||
}
|
||||
}
|
||||
)
|
||||
}) {
|
||||
Icon(Icons.Default.Camera, contentDescription = "Capture Image")
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
) {
|
||||
CameraPreview(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
controller = controller,
|
||||
onFrame = { bitmap ->
|
||||
if (state.isAutoCaptureOn) {
|
||||
viewModel.onEvent(CameraEvent.FrameReceived(bitmap))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Auto Capture")
|
||||
Switch(
|
||||
checked = state.isAutoCaptureOn,
|
||||
onCheckedChange = { viewModel.onEvent(CameraEvent.ToggleAutoCapture) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.example.livingai.pages.camera
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.livingai.domain.repository.CameraRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CameraViewModel(
|
||||
private val cameraRepository: CameraRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(CameraState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private var currentAnimalId: String = ""
|
||||
private var currentOrientation: String? = null
|
||||
|
||||
fun onEvent(event: CameraEvent) {
|
||||
when (event) {
|
||||
is CameraEvent.CaptureImage -> {
|
||||
// Signal UI to capture? Or handle if passed image
|
||||
}
|
||||
is CameraEvent.ImageCaptured -> {
|
||||
saveImage(event.imageProxy)
|
||||
}
|
||||
is CameraEvent.FrameReceived -> {
|
||||
processFrame(event.bitmap)
|
||||
}
|
||||
is CameraEvent.ToggleAutoCapture -> {
|
||||
_state.value = _state.value.copy(isAutoCaptureOn = !_state.value.isAutoCaptureOn)
|
||||
}
|
||||
is CameraEvent.ClearCapturedImage -> {
|
||||
_state.value = _state.value.copy(capturedImage = null, capturedImageUri = null)
|
||||
}
|
||||
is CameraEvent.SetContext -> {
|
||||
currentAnimalId = event.animalId
|
||||
currentOrientation = event.orientation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveImage(imageProxy: ImageProxy) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val bitmap = cameraRepository.captureImage(imageProxy)
|
||||
val uriString = cameraRepository.saveImage(bitmap, currentAnimalId, currentOrientation)
|
||||
_state.value = _state.value.copy(capturedImage = bitmap, capturedImageUri = Uri.parse(uriString))
|
||||
} catch (e: Exception) {
|
||||
// Handle error
|
||||
imageProxy.close() // Ensure closed on error if repository didn't
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processFrame(bitmap: Bitmap) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = cameraRepository.processFrame(bitmap)
|
||||
_state.value = _state.value.copy(inferenceResult = result)
|
||||
} catch (e: Exception) {
|
||||
// Handle error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class CameraState(
|
||||
val isLoading: Boolean = false,
|
||||
val isCameraReady: Boolean = false,
|
||||
val capturedImage: Any? = null,
|
||||
val capturedImageUri: Uri? = null,
|
||||
val inferenceResult: String? = null,
|
||||
val isAutoCaptureOn: Boolean = false
|
||||
)
|
||||
|
||||
sealed class CameraEvent {
|
||||
object CaptureImage : CameraEvent()
|
||||
data class ImageCaptured(val imageProxy: ImageProxy) : CameraEvent()
|
||||
data class FrameReceived(val bitmap: Bitmap) : CameraEvent()
|
||||
object ToggleAutoCapture : CameraEvent()
|
||||
object ClearCapturedImage : CameraEvent()
|
||||
data class SetContext(val animalId: String, val orientation: String?) : CameraEvent()
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
package com.example.livingai.pages.camera
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentValues
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.video.MediaStoreOutputOptions
|
||||
import androidx.camera.video.Recording
|
||||
import androidx.camera.video.VideoRecordEvent
|
||||
import androidx.camera.view.CameraController
|
||||
import androidx.camera.view.LifecycleCameraController
|
||||
import androidx.camera.view.video.AudioConfig
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Stop
|
||||
import androidx.compose.material.icons.filled.Videocam
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.navigation.NavController
|
||||
import com.example.livingai.pages.components.CameraPreview
|
||||
import com.example.livingai.pages.components.PermissionWrapper
|
||||
import com.example.livingai.pages.navigation.Route
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import java.io.File
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@Composable
|
||||
fun VideoRecordScreen(
|
||||
viewModel: VideoViewModel = koinViewModel(),
|
||||
navController: NavController,
|
||||
animalId: String
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val state by viewModel.state.collectAsState()
|
||||
var recording by remember { mutableStateOf<Recording?>(null) }
|
||||
|
||||
// We need RECORD_AUDIO permission for video with audio
|
||||
PermissionWrapper(
|
||||
permissions = listOf(
|
||||
Manifest.permission.CAMERA,
|
||||
Manifest.permission.RECORD_AUDIO
|
||||
)
|
||||
) {
|
||||
val controller = remember {
|
||||
LifecycleCameraController(context).apply {
|
||||
setEnabledUseCases(CameraController.VIDEO_CAPTURE)
|
||||
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.recordedVideoUri) {
|
||||
state.recordedVideoUri?.let { uri ->
|
||||
navController.navigate(Route.ViewVideoScreen(videoUri = uri.toString(), shouldAllowRetake = true, animalId = animalId))
|
||||
viewModel.onEvent(VideoEvent.ClearRecordedVideo)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (state.isRecording) {
|
||||
recording?.stop()
|
||||
viewModel.onEvent(VideoEvent.StopRecording)
|
||||
} else {
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, "${animalId}_video_${System.currentTimeMillis()}")
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
|
||||
if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.P) {
|
||||
put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/LivingAI/Media/$animalId")
|
||||
}
|
||||
}
|
||||
|
||||
val mediaStoreOutputOptions = MediaStoreOutputOptions.Builder(
|
||||
context.contentResolver,
|
||||
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||
)
|
||||
.setContentValues(contentValues)
|
||||
.build()
|
||||
|
||||
recording = controller.startRecording(
|
||||
mediaStoreOutputOptions,
|
||||
AudioConfig.create(true),
|
||||
ContextCompat.getMainExecutor(context)
|
||||
) { recordEvent: VideoRecordEvent ->
|
||||
when (recordEvent) {
|
||||
is VideoRecordEvent.Start -> {
|
||||
viewModel.onEvent(VideoEvent.StartRecording)
|
||||
}
|
||||
is VideoRecordEvent.Finalize -> {
|
||||
if (!recordEvent.hasError()) {
|
||||
val uri = recordEvent.outputResults.outputUri
|
||||
viewModel.onEvent(VideoEvent.VideoRecorded(uri))
|
||||
} else {
|
||||
viewModel.onEvent(VideoEvent.StopRecording)
|
||||
// Handle error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (state.isRecording) Icons.Default.Stop else Icons.Default.Videocam,
|
||||
contentDescription = if (state.isRecording) "Stop Recording" else "Start Recording"
|
||||
)
|
||||
}
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CameraPreview(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
controller = controller,
|
||||
onFrame = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.example.livingai.pages.camera
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class VideoViewModel : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(VideoState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
fun onEvent(event: VideoEvent) {
|
||||
when (event) {
|
||||
is VideoEvent.VideoRecorded -> {
|
||||
_state.value = _state.value.copy(recordedVideoUri = event.uri, isRecording = false)
|
||||
}
|
||||
is VideoEvent.StartRecording -> {
|
||||
_state.value = _state.value.copy(isRecording = true)
|
||||
}
|
||||
is VideoEvent.StopRecording -> {
|
||||
_state.value = _state.value.copy(isRecording = false)
|
||||
}
|
||||
is VideoEvent.ClearRecordedVideo -> {
|
||||
_state.value = _state.value.copy(recordedVideoUri = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class VideoState(
|
||||
val isRecording: Boolean = false,
|
||||
val recordedVideoUri: Uri? = null
|
||||
)
|
||||
|
||||
sealed class VideoEvent {
|
||||
data class VideoRecorded(val uri: Uri) : VideoEvent()
|
||||
object StartRecording : VideoEvent()
|
||||
object StopRecording : VideoEvent()
|
||||
object ClearRecordedVideo : VideoEvent()
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.example.livingai.pages.camera
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import com.example.livingai.ui.theme.LivingAITheme
|
||||
|
||||
@Composable
|
||||
fun ViewImageScreen(
|
||||
imageUri: String,
|
||||
shouldAllowRetake: Boolean,
|
||||
showAccept: Boolean,
|
||||
showBack: Boolean,
|
||||
onRetake: () -> Unit,
|
||||
onAccept: () -> Unit,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
LivingAITheme {
|
||||
Scaffold {
|
||||
Box(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)) {
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(model = Uri.parse(imageUri)),
|
||||
contentDescription = "Captured Image",
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
if (shouldAllowRetake) {
|
||||
OutlinedButton(onClick = onRetake) {
|
||||
Text("Retake")
|
||||
}
|
||||
}
|
||||
if (showAccept) {
|
||||
Button(onClick = onAccept) {
|
||||
Text("Accept")
|
||||
}
|
||||
} else {
|
||||
Button(onClick = onBack) {
|
||||
Text("Back")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
package com.example.livingai.pages.camera
|
||||
|
||||
import android.net.Uri
|
||||
import android.widget.VideoView
|
||||
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.Row
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.example.livingai.ui.theme.LivingAITheme
|
||||
|
||||
@Composable
|
||||
fun ViewVideoScreen(
|
||||
videoUri: String,
|
||||
shouldAllowRetake: Boolean,
|
||||
onRetake: () -> Unit,
|
||||
onAccept: () -> Unit,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
var isPlaying by remember { mutableStateOf(false) }
|
||||
var videoView: VideoView? by remember { mutableStateOf(null) }
|
||||
// Keep track if we have sought to the first frame to avoid resetting on recomposition repeatedly if not needed,
|
||||
// though VideoView state management in Compose can be tricky.
|
||||
var isPrepared by remember { mutableStateOf(false) }
|
||||
|
||||
LivingAITheme {
|
||||
Scaffold { padding ->
|
||||
Box(modifier = Modifier.fillMaxSize().padding(padding)) {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
VideoView(context).apply {
|
||||
setVideoURI(Uri.parse(videoUri))
|
||||
setOnPreparedListener { mp ->
|
||||
mp.isLooping = true
|
||||
isPrepared = true
|
||||
// Seek to 1ms to show thumbnail (first frame)
|
||||
seekTo(1)
|
||||
}
|
||||
setOnCompletionListener {
|
||||
isPlaying = false
|
||||
}
|
||||
}
|
||||
},
|
||||
update = { view ->
|
||||
videoView = view
|
||||
if (isPrepared) {
|
||||
if (isPlaying) {
|
||||
if (!view.isPlaying) view.start()
|
||||
} else {
|
||||
if (view.isPlaying) view.pause()
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable {
|
||||
isPlaying = !isPlaying
|
||||
}
|
||||
)
|
||||
|
||||
// Play Button Overlay (Only show when paused)
|
||||
if (!isPlaying) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable { isPlaying = true }, // Clicking anywhere while paused starts play
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = "Play",
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.background(Color.Black.copy(alpha = 0.4f), shape = MaterialTheme.shapes.medium)
|
||||
.padding(8.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Invisible clickable box to pause
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable { isPlaying = false },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// No icon, just allows pausing
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldAllowRetake) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
Button(onClick = onRetake) {
|
||||
Text("Retake")
|
||||
}
|
||||
Button(onClick = onAccept) {
|
||||
Text("Accept")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
IconButton(
|
||||
onClick = onBack,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
videoView?.stopPlayback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.example.livingai.pages.commons
|
||||
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
object Dimentions {
|
||||
//General
|
||||
val SMALL_PADDING = 8.dp
|
||||
val MEDIUM_PADDING = 16.dp
|
||||
val LARGE_PADDING = 24.dp
|
||||
val ICON_SIZE = 40.dp
|
||||
val BIG_ICON_SIZE = 100.dp
|
||||
|
||||
//TEXT Dimentions
|
||||
val SMALL_PADDING_TEXT = 12.dp
|
||||
val MEDIUM_PADDING_TEXT = 24.dp
|
||||
val LARGE_PADDING_TEXT = 36.dp
|
||||
|
||||
//IMAGE Dimentions
|
||||
val SMALL_PADDING_IMAGE = 12.dp
|
||||
val MEDIUM_PADDING_IMAGE = 24.dp
|
||||
val LARGE_PADDING_IMAGE = 36.dp
|
||||
|
||||
//BUTTON Dimentions
|
||||
val SMALL_PADDING_BUTTON = 12.dp
|
||||
val MEDIUM_PADDING_BUTTON = 24.dp
|
||||
val LARGE_PADDING_BUTTON = 36.dp
|
||||
|
||||
//INPUT Dimentions
|
||||
val SMALL_PADDING_INPUT = 12.dp
|
||||
val MEDIUM_PADDING_INPUT = 24.dp
|
||||
val LARGE_PADDING_INPUT = 36.dp
|
||||
|
||||
//COMPONENTS
|
||||
val INDICATOR_SIZE = 14.dp
|
||||
val OBJECT_SPACING = 36.dp
|
||||
val SMALL_OBJECT_LEN = 17.dp
|
||||
|
||||
val BOTTOM_BAR_ELEVATION = 5.dp
|
||||
val BOTTOM_BAR_PADDING = 4.dp
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
package com.example.livingai.pages.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BrokenImage
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.livingai.R
|
||||
import com.example.livingai.domain.model.AnimalProfile
|
||||
import com.example.livingai.pages.commons.Dimentions
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun AnimalProfileCard(
|
||||
animalProfile: AnimalProfile,
|
||||
onEdit: () -> Unit,
|
||||
onRate: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(Dimentions.SMALL_PADDING_TEXT),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(Dimentions.MEDIUM_PADDING_TEXT)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
// Left Side: Animal Details
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = Dimentions.SMALL_PADDING_TEXT)
|
||||
) {
|
||||
Text(
|
||||
text = animalProfile.name,
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "${stringResource(R.string.label_species)}: ${animalProfile.species}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(R.string.label_breed)}: ${animalProfile.breed}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(R.string.label_age)}: ${animalProfile.age}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(R.string.label_sex)}: ${animalProfile.sex}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
// Right Side: Image Thumbnails with Dots
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
val images = if (animalProfile.imageUrls.isNotEmpty()) animalProfile.imageUrls else listOf("")
|
||||
val pagerState = rememberPagerState(pageCount = { images.size })
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(120.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color.LightGray)
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) { page ->
|
||||
val imageUrl = images[page]
|
||||
if (imageUrl.isNotEmpty()) {
|
||||
ThumbnailImage(
|
||||
imageUri = Uri.parse(imageUrl),
|
||||
onClick = { /* No action for now */ },
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
applySizeModifier = false
|
||||
)
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.BrokenImage,
|
||||
contentDescription = null,
|
||||
tint = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Dots Indicator
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
repeat(images.size) { iteration ->
|
||||
val color = if (pagerState.currentPage == iteration)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.outlineVariant
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(2.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
.size(6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom: Action Buttons
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = Dimentions.MEDIUM_PADDING_TEXT, vertical = Dimentions.SMALL_PADDING_TEXT),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onEdit,
|
||||
modifier = Modifier.weight(1f).padding(end = 4.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.edit_button))
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = onRate,
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 4.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.rate_button))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onDelete,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error),
|
||||
modifier = Modifier.weight(1f).padding(start = 4.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.delete_button))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.example.livingai.pages.components
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.view.ViewGroup
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.view.LifecycleCameraController
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@Composable
|
||||
fun CameraPreview(
|
||||
modifier: Modifier = Modifier,
|
||||
controller: LifecycleCameraController? = null,
|
||||
onFrame: (Bitmap) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val cameraExecutor = remember { Executors.newSingleThreadExecutor() }
|
||||
|
||||
val cameraController = controller ?: remember { LifecycleCameraController(context) }
|
||||
|
||||
LaunchedEffect(cameraController) {
|
||||
cameraController.setImageAnalysisAnalyzer(cameraExecutor) { imageProxy ->
|
||||
val bitmap = imageProxy.toBitmap()
|
||||
onFrame(bitmap)
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure default setup if it was created internally, or even if passed externally we might want to enforce defaults
|
||||
// But typically caller configures it if they pass it.
|
||||
// However, for this component to work as a preview + analysis, we should ensure analysis is enabled.
|
||||
|
||||
if (controller == null) {
|
||||
// Only set defaults if we created it
|
||||
LaunchedEffect(cameraController) {
|
||||
cameraController.setEnabledUseCases(LifecycleCameraController.IMAGE_ANALYSIS or LifecycleCameraController.IMAGE_CAPTURE or LifecycleCameraController.VIDEO_CAPTURE)
|
||||
cameraController.cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||
cameraController.bindToLifecycle(lifecycleOwner)
|
||||
}
|
||||
} else {
|
||||
// If passed externally, we still need to bind it if not already bound?
|
||||
// Usually the caller binds it. But let's be safe or assume caller does it.
|
||||
// Actually, let's bind it here to be sure, as this component represents the "Active Camera".
|
||||
LaunchedEffect(cameraController, lifecycleOwner) {
|
||||
cameraController.bindToLifecycle(lifecycleOwner)
|
||||
}
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = { ctx ->
|
||||
PreviewView(ctx).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
this.controller = cameraController
|
||||
}
|
||||
},
|
||||
onRelease = {
|
||||
// Cleanup if needed
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package com.example.livingai.pages.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.List
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.navigation.NavController
|
||||
import com.example.livingai.pages.navigation.Route
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CommonScaffold(
|
||||
navController: NavController,
|
||||
title: String,
|
||||
content: @Composable (PaddingValues) -> Unit
|
||||
) {
|
||||
val bottomNavItems = listOf(
|
||||
BottomBarItem(
|
||||
route = Route.OnBoardingScreen,
|
||||
icon = Icons.Default.Build,
|
||||
name = "Nav",
|
||||
notifications = 1,
|
||||
),
|
||||
BottomBarItem(
|
||||
route = Route.HomeScreen,
|
||||
icon = Icons.Default.Home,
|
||||
name = "Home",
|
||||
notifications = 5,
|
||||
),
|
||||
BottomBarItem(
|
||||
route = Route.ListingsScreen,
|
||||
icon = Icons.Default.List,
|
||||
name = "Listings",
|
||||
notifications = 0,
|
||||
)
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold)
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
titleContentColor = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
LivingAIBottomBar(
|
||||
navItems = bottomNavItems,
|
||||
navController = navController,
|
||||
onClick = { navController.navigate(it.route) }
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
content(innerPadding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.example.livingai.pages.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
||||
@Composable
|
||||
fun FlexButton(
|
||||
@StringRes text: Int,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(text),
|
||||
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FlexTextButton(
|
||||
@StringRes text: Int,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
TextButton(onClick = onClick) {
|
||||
Text(
|
||||
text = stringResource(text),
|
||||
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.example.livingai.pages.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.border
|
||||
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.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddAPhoto
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.livingai.R
|
||||
import com.example.livingai.pages.commons.Dimentions
|
||||
|
||||
@Composable
|
||||
fun ImageThumbnailButton(
|
||||
image: Any?,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@StringRes labelRes: Int? = null
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(Dimentions.SMALL_PADDING))
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||
RoundedCornerShape(Dimentions.SMALL_PADDING)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (image != null) {
|
||||
val uri = if (image is Uri) image else Uri.parse(image.toString())
|
||||
ThumbnailImage(
|
||||
imageUri = uri,
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
applySizeModifier = false,
|
||||
requestSizePx = 256
|
||||
)
|
||||
} else {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable { onClick() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.AddAPhoto,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.label_add_photo),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||
modifier = Modifier.padding(top = Dimentions.SMALL_PADDING)
|
||||
)
|
||||
if (labelRes != null) {
|
||||
Text(
|
||||
text = stringResource(id = labelRes),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.example.livingai.pages.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LabeledDropdown(
|
||||
@StringRes labelRes: Int,
|
||||
options: List<String>,
|
||||
selected: String?,
|
||||
onSelected: (String) -> Unit
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Column {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = !expanded }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selected ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(labelRes)) },
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
|
||||
},
|
||||
modifier = Modifier
|
||||
.menuAnchor(MenuAnchorType.PrimaryEditable, true)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer
|
||||
) {
|
||||
options.forEach { item ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(item) },
|
||||
onClick = {
|
||||
onSelected(item)
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.example.livingai.pages.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
|
||||
@Composable
|
||||
fun LabeledTextField(
|
||||
@StringRes labelRes: Int,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
keyboardType: KeyboardType = KeyboardType.Text
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = { Text(stringResource(id = labelRes)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = keyboardType)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
package com.example.livingai.pages.components
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.List
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationBarItemDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.example.livingai.pages.commons.Dimentions
|
||||
import com.example.livingai.pages.navigation.Route
|
||||
import com.example.livingai.ui.theme.LivingAITheme
|
||||
|
||||
@Composable
|
||||
fun LivingAIBottomBar(
|
||||
navItems: List<BottomBarItem>,
|
||||
navController: NavController,
|
||||
onClick: (BottomBarItem) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val backStackEntry = navController.currentBackStackEntryAsState()
|
||||
val currentRoute = backStackEntry.value?.destination?.route
|
||||
|
||||
NavigationBar(
|
||||
modifier = modifier,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
tonalElevation = Dimentions.BOTTOM_BAR_ELEVATION
|
||||
) {
|
||||
navItems.forEach { item ->
|
||||
val sel = ((currentRoute != null) && (item.route::class == currentRoute::class))
|
||||
NavigationBarItem(
|
||||
selected = sel,
|
||||
onClick = { onClick(item) },
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onSurface,
|
||||
indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
),
|
||||
icon = {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (item.notifications > 0) {
|
||||
Badge(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError
|
||||
) {
|
||||
Text(
|
||||
text = item.notifications.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
content = {
|
||||
Icon(
|
||||
imageVector = item.icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(Dimentions.BOTTOM_BAR_PADDING)
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
label = { Text(text = item.name) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class BottomBarItem(
|
||||
val route: Route,
|
||||
val icon: ImageVector,
|
||||
val name: String,
|
||||
val notifications: Int
|
||||
)
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun showBottomBar() {
|
||||
val bottomNavItems = listOf(
|
||||
BottomBarItem(
|
||||
route = Route.OnBoardingScreen,
|
||||
icon = Icons.Default.Build,
|
||||
name = "Nav",
|
||||
notifications = 1,
|
||||
),
|
||||
BottomBarItem(
|
||||
route = Route.HomeScreen,
|
||||
icon = Icons.Default.Home,
|
||||
name = "Home",
|
||||
notifications = 5,
|
||||
),
|
||||
BottomBarItem(
|
||||
route = Route.ListingsScreen,
|
||||
icon = Icons.Default.List,
|
||||
name = "Listings",
|
||||
notifications = 0,
|
||||
)
|
||||
)
|
||||
LivingAITheme {
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
LivingAIBottomBar(
|
||||
navItems = bottomNavItems,
|
||||
navController = rememberNavController(),
|
||||
onClick = {},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Text(
|
||||
text = "test page",
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.example.livingai.pages.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.example.livingai.pages.commons.Dimentions
|
||||
import com.example.livingai.pages.onboarding.Page
|
||||
import com.example.livingai.pages.onboarding.pages
|
||||
import com.example.livingai.ui.theme.LivingAITheme
|
||||
|
||||
@Composable
|
||||
fun OnBoardingPage(
|
||||
modifier: Modifier = Modifier,
|
||||
page: Page
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(fraction = 0.60F),
|
||||
painter = painterResource(id = page.image),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = Dimentions.MEDIUM_PADDING_TEXT))
|
||||
Text(
|
||||
text = stringResource(page.title),
|
||||
modifier = Modifier.padding(horizontal = Dimentions.MEDIUM_PADDING_TEXT),
|
||||
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold),
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
Text(
|
||||
text = stringResource(page.description),
|
||||
modifier = Modifier.padding(horizontal = Dimentions.MEDIUM_PADDING_TEXT),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun OnBoardingPagePreview() {
|
||||
LivingAITheme {
|
||||
OnBoardingPage(
|
||||
page = pages[0]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.example.livingai.pages.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.example.livingai.pages.commons.Dimentions.INDICATOR_SIZE
|
||||
|
||||
@Composable
|
||||
fun PageIndicator(
|
||||
modifier: Modifier = Modifier,
|
||||
pageSize: Int,
|
||||
selectedPage: Int,
|
||||
selectedColor: Color = MaterialTheme.colorScheme.primary,
|
||||
unselectedColor: Color = MaterialTheme.colorScheme.outlineVariant
|
||||
) {
|
||||
Row(modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
repeat(pageSize) { page ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(INDICATOR_SIZE)
|
||||
.clip(CircleShape)
|
||||
.background(color = if (page == selectedPage) selectedColor else unselectedColor)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.example.livingai.pages.components
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
@Composable
|
||||
fun PermissionWrapper(
|
||||
permissions: List<String> = listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO),
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var arePermissionsGranted by remember {
|
||||
mutableStateOf(
|
||||
permissions.all {
|
||||
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { results ->
|
||||
arePermissionsGranted = results.values.all { it }
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = true) {
|
||||
if (!arePermissionsGranted) {
|
||||
launcher.launch(permissions.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
if (arePermissionsGranted) {
|
||||
content()
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Button(onClick = { launcher.launch(permissions.toTypedArray()) }) {
|
||||
Text("Grant Permissions")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.example.livingai.pages.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
||||
@Composable
|
||||
fun RadioGroup(
|
||||
@StringRes titleRes: Int,
|
||||
options: List<String>,
|
||||
selected: String?,
|
||||
onSelected: (String) -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = stringResource(id = titleRes),
|
||||
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
options.forEach { option ->
|
||||
Row(
|
||||
modifier = Modifier.clickable { onSelected(option) },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selected == option,
|
||||
onClick = { onSelected(option) }
|
||||
)
|
||||
Text(
|
||||
text = option,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package com.example.livingai.pages.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.livingai.pages.commons.Dimentions
|
||||
|
||||
@Composable
|
||||
fun RatingScale(
|
||||
@StringRes label: Int,
|
||||
value: Int,
|
||||
maxValue: Int,
|
||||
onValueChange: (Int) -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = Dimentions.SMALL_PADDING_TEXT)) {
|
||||
Text(
|
||||
text = stringResource(id = label),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(bottom = Dimentions.SMALL_PADDING)
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(Dimentions.SMALL_PADDING))
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||
RoundedCornerShape(Dimentions.SMALL_PADDING)
|
||||
)
|
||||
.padding(Dimentions.SMALL_PADDING),
|
||||
horizontalArrangement = Arrangement.SpaceAround,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
for (i in 1..maxValue) {
|
||||
val isSelected = i == value
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(Dimentions.ICON_SIZE)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface
|
||||
)
|
||||
.border(1.dp, MaterialTheme.colorScheme.primary, CircleShape)
|
||||
.clickable { onValueChange(i) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = i.toString(),
|
||||
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.example.livingai.pages.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.request.ImageRequest
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun ThumbnailImage(
|
||||
imageUri: Uri,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
thumbnailSize: Dp = 120.dp,
|
||||
applySizeModifier: Boolean = true,
|
||||
requestSizePx: Int? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val sizeInPx = requestSizePx ?: with(LocalDensity.current) { thumbnailSize.toPx().roundToInt() }
|
||||
|
||||
var finalModifier = modifier.clickable { onClick() }
|
||||
if (applySizeModifier) {
|
||||
finalModifier = finalModifier.size(thumbnailSize)
|
||||
}
|
||||
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(
|
||||
ImageRequest.Builder(context)
|
||||
.data(imageUri)
|
||||
.size(sizeInPx)
|
||||
.crossfade(true)
|
||||
.build()
|
||||
),
|
||||
contentDescription = "Thumbnail",
|
||||
modifier = finalModifier,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package com.example.livingai.pages.components
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
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.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Videocam
|
||||
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.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.ImageLoader
|
||||
import coil.compose.AsyncImage
|
||||
import coil.decode.VideoFrameDecoder
|
||||
import com.example.livingai.R
|
||||
import com.example.livingai.pages.commons.Dimentions
|
||||
|
||||
@Composable
|
||||
fun VideoThumbnailButton(
|
||||
videoSource: Any?,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val imageLoader = ImageLoader.Builder(context)
|
||||
.components {
|
||||
add(VideoFrameDecoder.Factory())
|
||||
}
|
||||
.build()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(Dimentions.SMALL_PADDING))
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||
RoundedCornerShape(Dimentions.SMALL_PADDING)
|
||||
)
|
||||
.clickable { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (videoSource != null) {
|
||||
AsyncImage(
|
||||
model = videoSource,
|
||||
imageLoader = imageLoader,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
// Overlay video icon to indicate it's a video
|
||||
Icon(
|
||||
imageVector = Icons.Default.Videocam,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
} else {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Videocam,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.label_add_video),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||
modifier = Modifier.padding(top = Dimentions.SMALL_PADDING)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.example.livingai.pages.home
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.navigation.NavController
|
||||
import com.example.livingai.R
|
||||
import com.example.livingai.pages.commons.Dimentions
|
||||
import com.example.livingai.pages.navigation.Route
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeScreen(navController: NavController) {
|
||||
Scaffold { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.padding(Dimentions.SMALL_PADDING_TEXT),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(Dimentions.BIG_ICON_SIZE)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.app_name),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(Dimentions.MEDIUM_PADDING))
|
||||
|
||||
HomeButton(
|
||||
text = stringResource(id = R.string.top_bar_add_profile),
|
||||
onClick = { navController.navigate(Route.AddProfileScreen()) }
|
||||
)
|
||||
HomeButton(
|
||||
text = stringResource(id = R.string.top_bar_listings),
|
||||
onClick = { navController.navigate(Route.ListingsScreen) }
|
||||
)
|
||||
HomeButton(
|
||||
text = stringResource(id = R.string.top_bar_settings),
|
||||
onClick = { navController.navigate(Route.SettingsScreen) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomeButton(
|
||||
text: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = Dimentions.SMALL_PADDING_TEXT)
|
||||
) {
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.example.livingai.pages.home
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.livingai.domain.usecases.AppEntry.AppEntryUseCases
|
||||
import com.example.livingai.pages.navigation.Route
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class HomeViewModel(
|
||||
private val appEntryUseCases: AppEntryUseCases
|
||||
): ViewModel() {
|
||||
private val _splashCondition = mutableStateOf(true)
|
||||
val splashCondition: State<Boolean> = _splashCondition
|
||||
|
||||
private val _startDestination = mutableStateOf<Route>(Route.AppStartNavigation)
|
||||
val startDestination: State<Route> = _startDestination
|
||||
|
||||
init {
|
||||
appEntryUseCases.readAppEntry().onEach { shouldStartFromHomeScreen ->
|
||||
if(shouldStartFromHomeScreen){
|
||||
_startDestination.value = Route.HomeNavigation
|
||||
}else{
|
||||
_startDestination.value = Route.AppStartNavigation
|
||||
}
|
||||
delay(350) //Without this delay, the onBoarding screen will show for a momentum.
|
||||
_splashCondition.value = false
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.example.livingai.pages.listings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavController
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.example.livingai.R
|
||||
import com.example.livingai.domain.model.AnimalProfile
|
||||
import com.example.livingai.pages.commons.Dimentions
|
||||
import com.example.livingai.pages.components.AnimalProfileCard
|
||||
import com.example.livingai.pages.components.CommonScaffold
|
||||
import com.example.livingai.pages.navigation.Route
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ListingsScreen(
|
||||
navController: NavController,
|
||||
viewModel: ListingsViewModel = koinViewModel()
|
||||
) {
|
||||
val animalProfiles: LazyPagingItems<AnimalProfile> = viewModel.animalProfiles.collectAsLazyPagingItems()
|
||||
|
||||
CommonScaffold(
|
||||
navController = navController,
|
||||
title = stringResource(R.string.top_bar_listings)
|
||||
) { innerPadding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
contentPadding = PaddingValues(Dimentions.SMALL_PADDING_TEXT),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimentions.SMALL_PADDING_TEXT)
|
||||
) {
|
||||
items(count = animalProfiles.itemCount) { index ->
|
||||
val item = animalProfiles[index]
|
||||
item?.let { profile ->
|
||||
AnimalProfileCard(
|
||||
animalProfile = profile,
|
||||
onEdit = {
|
||||
navController.navigate(Route.AddProfileScreen(animalId = profile.animalId, loadEntry = true))
|
||||
},
|
||||
onRate = { navController.navigate(Route.RatingScreen(animalId = profile.animalId)) },
|
||||
onDelete = { viewModel.deleteAnimalProfile(profile.animalId) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.example.livingai.pages.listings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.cachedIn
|
||||
import com.example.livingai.domain.usecases.ProfileListing.ProfileListingUseCase
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ListingsViewModel(
|
||||
private val profileListingUseCase: ProfileListingUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
val animalProfiles = profileListingUseCase.getAnimalProfiles().cachedIn(viewModelScope)
|
||||
|
||||
fun deleteAnimalProfile(animalId: String) {
|
||||
viewModelScope.launch {
|
||||
profileListingUseCase.deleteAnimalProfile(animalId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
package com.example.livingai.pages.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.toRoute
|
||||
import com.example.livingai.pages.addprofile.AddProfileScreen
|
||||
import com.example.livingai.pages.addprofile.AddProfileViewModel
|
||||
import com.example.livingai.pages.camera.CameraScreen
|
||||
import com.example.livingai.pages.camera.VideoRecordScreen
|
||||
import com.example.livingai.pages.camera.ViewImageScreen
|
||||
import com.example.livingai.pages.camera.ViewVideoScreen
|
||||
import com.example.livingai.pages.home.HomeScreen
|
||||
import com.example.livingai.pages.listings.ListingsScreen
|
||||
import com.example.livingai.pages.onboarding.OnBoardingScreen
|
||||
import com.example.livingai.pages.onboarding.OnBoardingViewModel
|
||||
import com.example.livingai.pages.ratings.RatingScreen
|
||||
import com.example.livingai.pages.settings.SettingsScreen
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun NavGraph(
|
||||
startDestination: Route
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
NavHost(navController = navController, startDestination = startDestination) {
|
||||
navigation<Route.AppStartNavigation>(
|
||||
startDestination = Route.OnBoardingScreen
|
||||
) {
|
||||
composable<Route.OnBoardingScreen> {
|
||||
val onBoardingViewModel: OnBoardingViewModel = koinViewModel()
|
||||
OnBoardingScreen(
|
||||
events = onBoardingViewModel::onEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
navigation<Route.HomeNavigation>(
|
||||
startDestination = Route.HomeScreen
|
||||
) {
|
||||
composable<Route.HomeScreen> {
|
||||
HomeScreen(navController = navController)
|
||||
}
|
||||
|
||||
composable<Route.ListingsScreen> {
|
||||
ListingsScreen(
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
|
||||
composable<Route.AddProfileScreen> { backStackEntry ->
|
||||
val route: Route.AddProfileScreen = backStackEntry.toRoute()
|
||||
val viewModel: AddProfileViewModel = koinViewModel()
|
||||
val currentId by viewModel.currentAnimalId
|
||||
|
||||
LaunchedEffect(route.animalId, route.loadEntry) {
|
||||
if (route.loadEntry) {
|
||||
viewModel.loadAnimalDetails(route.animalId)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle new media from saved state handle
|
||||
val newImageUri = backStackEntry.savedStateHandle.get<String>("newImageUri")
|
||||
val newImageOrientation = backStackEntry.savedStateHandle.get<String>("newImageOrientation")
|
||||
val newVideoUri = backStackEntry.savedStateHandle.get<String>("newVideoUri")
|
||||
|
||||
LaunchedEffect(newImageUri, newImageOrientation) {
|
||||
if (newImageUri != null && newImageOrientation != null) {
|
||||
viewModel.addPhoto(newImageOrientation, newImageUri)
|
||||
backStackEntry.savedStateHandle.remove<String>("newImageUri")
|
||||
backStackEntry.savedStateHandle.remove<String>("newImageOrientation")
|
||||
}
|
||||
}
|
||||
LaunchedEffect(newVideoUri) {
|
||||
if (newVideoUri != null) {
|
||||
viewModel.setVideo(newVideoUri)
|
||||
backStackEntry.savedStateHandle.remove<String>("newVideoUri")
|
||||
}
|
||||
}
|
||||
|
||||
val photos = viewModel.photos
|
||||
|
||||
AddProfileScreen(
|
||||
navController = navController,
|
||||
viewModel = viewModel,
|
||||
onSave = {
|
||||
viewModel.saveAnimalDetails()
|
||||
navController.previousBackStackEntry?.savedStateHandle?.set("refresh_listings", true)
|
||||
navController.popBackStack()
|
||||
},
|
||||
onCancel = { navController.popBackStack() },
|
||||
onTakePhoto = { orientation ->
|
||||
val existingPhoto = photos[orientation]
|
||||
if (existingPhoto != null) {
|
||||
navController.navigate(Route.ViewImageScreen(
|
||||
imageUri = existingPhoto,
|
||||
shouldAllowRetake = true,
|
||||
orientation = orientation,
|
||||
showAccept = false,
|
||||
animalId = currentId ?: "unknown"
|
||||
))
|
||||
} else {
|
||||
navController.navigate(Route.CameraScreen(orientation = orientation, animalId = currentId ?: "unknown"))
|
||||
}
|
||||
},
|
||||
onTakeVideo = { navController.navigate(Route.VideoRecordScreen(animalId = currentId ?: "unknown")) }
|
||||
)
|
||||
}
|
||||
|
||||
composable<Route.RatingScreen> {
|
||||
RatingScreen()
|
||||
}
|
||||
|
||||
composable<Route.SettingsScreen> {
|
||||
SettingsScreen(navController = navController)
|
||||
}
|
||||
|
||||
composable<Route.CameraScreen> { backStackEntry ->
|
||||
val route: Route.CameraScreen = backStackEntry.toRoute()
|
||||
CameraScreen(navController = navController, orientation = route.orientation, animalId = route.animalId)
|
||||
}
|
||||
|
||||
composable<Route.VideoRecordScreen> { backStackEntry ->
|
||||
val route: Route.VideoRecordScreen = backStackEntry.toRoute()
|
||||
VideoRecordScreen(navController = navController, animalId = route.animalId)
|
||||
}
|
||||
|
||||
composable<Route.ViewImageScreen> { backStackEntry ->
|
||||
val args: Route.ViewImageScreen = backStackEntry.toRoute()
|
||||
ViewImageScreen(
|
||||
imageUri = args.imageUri,
|
||||
shouldAllowRetake = args.shouldAllowRetake,
|
||||
showAccept = args.showAccept,
|
||||
showBack = args.showBack,
|
||||
onRetake = {
|
||||
navController.popBackStack()
|
||||
navController.navigate(Route.CameraScreen(orientation = args.orientation, animalId = args.animalId))
|
||||
},
|
||||
onAccept = {
|
||||
navController.getBackStackEntry<Route.AddProfileScreen>().savedStateHandle["newImageUri"] = args.imageUri
|
||||
navController.getBackStackEntry<Route.AddProfileScreen>().savedStateHandle["newImageOrientation"] = args.orientation
|
||||
navController.popBackStack<Route.AddProfileScreen>(inclusive = false)
|
||||
},
|
||||
onBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
composable<Route.ViewVideoScreen> { backStackEntry ->
|
||||
val args: Route.ViewVideoScreen = backStackEntry.toRoute()
|
||||
ViewVideoScreen(
|
||||
videoUri = args.videoUri,
|
||||
shouldAllowRetake = args.shouldAllowRetake,
|
||||
onRetake = {
|
||||
navController.popBackStack()
|
||||
navController.navigate(Route.VideoRecordScreen(animalId = args.animalId))
|
||||
},
|
||||
onAccept = {
|
||||
navController.getBackStackEntry<Route.AddProfileScreen>().savedStateHandle["newVideoUri"] = args.videoUri
|
||||
navController.popBackStack<Route.AddProfileScreen>(inclusive = false)
|
||||
},
|
||||
onBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.example.livingai.pages.navigation
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed class Route {
|
||||
@Serializable
|
||||
object AppStartNavigation : Route()
|
||||
@Serializable
|
||||
object HomeNavigation : Route()
|
||||
@Serializable
|
||||
object OnBoardingScreen : Route()
|
||||
@Serializable
|
||||
object HomeScreen : Route()
|
||||
@Serializable
|
||||
object ListingsScreen : Route()
|
||||
@Serializable
|
||||
data class AddProfileScreen(val animalId: String? = null, val loadEntry: Boolean = false) : Route()
|
||||
@Serializable
|
||||
data class RatingScreen(val animalId: String) : Route()
|
||||
@Serializable
|
||||
data class CameraScreen(val orientation: String? = null, val animalId: String) : Route()
|
||||
@Serializable
|
||||
data class VideoRecordScreen(val animalId: String) : Route()
|
||||
@Serializable
|
||||
data class ViewImageScreen(val imageUri: String, val shouldAllowRetake: Boolean, val orientation: String? = null, val showAccept: Boolean = false, val showBack: Boolean = false, val animalId: String) : Route()
|
||||
@Serializable
|
||||
data class ViewVideoScreen(val videoUri: String, val shouldAllowRetake: Boolean, val animalId: String) : Route()
|
||||
@Serializable
|
||||
object SettingsScreen : Route()
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.example.livingai.pages.onboarding
|
||||
|
||||
sealed class OnBoardingEvents {
|
||||
object SaveAppEntry: OnBoardingEvents()
|
||||
data class SaveLanguage(val language: String): OnBoardingEvents()
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.example.livingai.pages.onboarding
|
||||
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.example.livingai.R
|
||||
import com.example.livingai.pages.commons.Dimentions.OBJECT_SPACING
|
||||
import com.example.livingai.pages.commons.Dimentions.SMALL_OBJECT_LEN
|
||||
import com.example.livingai.pages.commons.Dimentions.SMALL_PADDING_BUTTON
|
||||
import com.example.livingai.pages.components.FlexButton
|
||||
import com.example.livingai.pages.components.FlexTextButton
|
||||
import com.example.livingai.pages.components.OnBoardingPage
|
||||
import com.example.livingai.pages.components.PageIndicator
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun OnBoardingScreen(
|
||||
events: (OnBoardingEvents) -> Unit
|
||||
) {
|
||||
val pageLen = pages.size
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
val pagerState = rememberPagerState(initialPage = 0) {
|
||||
pages.size
|
||||
}
|
||||
val buttonState = remember {
|
||||
derivedStateOf {
|
||||
when (pagerState.currentPage) {
|
||||
0 -> listOf(R.string.english_language, R.string.hindi_language)
|
||||
pageLen - 1 -> listOf(R.string.back_button, R.string.get_started_button)
|
||||
else -> listOf(R.string.back_button, R.string.next_button)
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalPager(state = pagerState) {
|
||||
OnBoardingPage(page = pages[it])
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1F))
|
||||
Row(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = OBJECT_SPACING)
|
||||
.navigationBarsPadding(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
PageIndicator(
|
||||
modifier = Modifier.width(SMALL_OBJECT_LEN * pageLen),
|
||||
pageSize = pageLen,
|
||||
selectedPage = pagerState.currentPage
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
if (pagerState.currentPage != 0) {
|
||||
FlexTextButton(
|
||||
text = buttonState.value[0],
|
||||
onClick = {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(page = pagerState.currentPage - 1)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
FlexButton(
|
||||
text = buttonState.value[0],
|
||||
onClick = {
|
||||
events(OnBoardingEvents.SaveLanguage("en"))
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(page = pagerState.currentPage + 1)
|
||||
}
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.width(SMALL_PADDING_BUTTON))
|
||||
}
|
||||
|
||||
FlexButton(
|
||||
text = buttonState.value[1],
|
||||
onClick = {
|
||||
if (pagerState.currentPage == 0) {
|
||||
events(OnBoardingEvents.SaveLanguage("hi"))
|
||||
}
|
||||
|
||||
if (pagerState.currentPage + 1 >= pageLen) {
|
||||
events(OnBoardingEvents.SaveAppEntry)
|
||||
} else {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(page = pagerState.currentPage + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.example.livingai.pages.onboarding
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.livingai.data.local.model.SettingsData
|
||||
import com.example.livingai.domain.repository.SettingsRepository
|
||||
import com.example.livingai.domain.usecases.AppEntry.AppEntryUseCases
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class OnBoardingViewModel(
|
||||
private val appEntryUseCases: AppEntryUseCases,
|
||||
private val settingsRepository: SettingsRepository
|
||||
): ViewModel() {
|
||||
fun onEvent(event: OnBoardingEvents) {
|
||||
when(event) {
|
||||
is OnBoardingEvents.SaveAppEntry -> {
|
||||
saveAppEntry()
|
||||
}
|
||||
is OnBoardingEvents.SaveLanguage -> {
|
||||
saveLanguage(event.language)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAppEntry() {
|
||||
viewModelScope.launch {
|
||||
appEntryUseCases.saveAppEntry()
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveLanguage(language: String) {
|
||||
viewModelScope.launch {
|
||||
// Preserving default for other settings or we could fetch existing if needed
|
||||
// For onboarding, assuming defaults for others is fine.
|
||||
settingsRepository.saveSettings(SettingsData(language = language, isAutoCaptureOn = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.example.livingai.pages.onboarding
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.example.livingai.R
|
||||
|
||||
data class Page(
|
||||
@StringRes val title: Int,
|
||||
@StringRes val description: Int,
|
||||
@DrawableRes val image: Int
|
||||
)
|
||||
|
||||
val pages = listOf(
|
||||
Page(
|
||||
title = R.string.language_onboarding_title,
|
||||
description = R.string.language_onboarding_description,
|
||||
image = R.drawable.splash
|
||||
),
|
||||
Page(
|
||||
title = R.string.buy_onboarding_title,
|
||||
description = R.string.buy_onboarding_description,
|
||||
image = R.drawable.splash
|
||||
),
|
||||
Page(
|
||||
title = R.string.sell_onboarding_title,
|
||||
description = R.string.sell_onboarding_description,
|
||||
image = R.drawable.splash
|
||||
),
|
||||
Page(
|
||||
title = R.string.amenities_onboarding_title,
|
||||
description = R.string.amenities_onboarding_description,
|
||||
image = R.drawable.splash
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.example.livingai.pages.ratings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavController
|
||||
import com.example.livingai.R
|
||||
import com.example.livingai.pages.commons.Dimentions
|
||||
import com.example.livingai.pages.components.ImageThumbnailButton
|
||||
import com.example.livingai.pages.components.RatingScale
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RatingScreen(
|
||||
navController: NavController? = null, // Nullable for preview or if passed from NavGraph
|
||||
viewModel: RatingViewModel = koinViewModel()
|
||||
) {
|
||||
val ratingState by viewModel.ratingState.collectAsState()
|
||||
val animalImages by viewModel.animalImages.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
// You might want a TopAppBar here, reusing CommonScaffold or similar
|
||||
Text(
|
||||
text = stringResource(id = R.string.top_bar_ratings),
|
||||
modifier = Modifier.padding(Dimentions.MEDIUM_PADDING)
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
ratingState?.let { rating ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.padding(horizontal = Dimentions.SMALL_PADDING_TEXT),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimentions.SMALL_PADDING)
|
||||
) {
|
||||
// 1. Image Thumbnail
|
||||
item {
|
||||
if (animalImages.isNotEmpty()) {
|
||||
ImageThumbnailButton(
|
||||
image = animalImages.firstOrNull(),
|
||||
onClick = { /* TODO: Handle click */ }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Description / Comments Box
|
||||
item {
|
||||
OutlinedTextField(
|
||||
value = rating.bodyConditionComments,
|
||||
onValueChange = { viewModel.onRatingChange(rating.copy(bodyConditionComments = it)) },
|
||||
label = { Text(stringResource(id = R.string.label_rating_comments)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 3
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Rating Components
|
||||
item { RatingScale(R.string.label_overall_rating, rating.overallRating, 10) { viewModel.onRatingChange(rating.copy(overallRating = it)) } }
|
||||
item { RatingScale(R.string.label_health_rating, rating.healthRating, 10) { viewModel.onRatingChange(rating.copy(healthRating = it)) } }
|
||||
item { RatingScale(R.string.label_breed_rating, rating.breedRating, 10) { viewModel.onRatingChange(rating.copy(breedRating = it)) } }
|
||||
|
||||
// Physical Attributes
|
||||
item { RatingScale(R.string.label_stature, rating.stature, 10) { viewModel.onRatingChange(rating.copy(stature = it)) } }
|
||||
item { RatingScale(R.string.label_chest_width, rating.chestWidth, 10) { viewModel.onRatingChange(rating.copy(chestWidth = it)) } }
|
||||
item { RatingScale(R.string.label_body_depth, rating.bodyDepth, 10) { viewModel.onRatingChange(rating.copy(bodyDepth = it)) } }
|
||||
item { RatingScale(R.string.label_angularity, rating.angularity, 10) { viewModel.onRatingChange(rating.copy(angularity = it)) } }
|
||||
item { RatingScale(R.string.label_rump_angle, rating.rumpAngle, 10) { viewModel.onRatingChange(rating.copy(rumpAngle = it)) } }
|
||||
item { RatingScale(R.string.label_rump_width, rating.rumpWidth, 10) { viewModel.onRatingChange(rating.copy(rumpWidth = it)) } }
|
||||
item { RatingScale(R.string.label_rear_leg_set, rating.rearLegSet, 10) { viewModel.onRatingChange(rating.copy(rearLegSet = it)) } }
|
||||
item { RatingScale(R.string.label_rear_leg_rear_view, rating.rearLegRearView, 10) { viewModel.onRatingChange(rating.copy(rearLegRearView = it)) } }
|
||||
item { RatingScale(R.string.label_foot_angle, rating.footAngle, 10) { viewModel.onRatingChange(rating.copy(footAngle = it)) } }
|
||||
|
||||
// Udder Attributes
|
||||
item { RatingScale(R.string.label_fore_udder_attachment, rating.foreUdderAttachment, 10) { viewModel.onRatingChange(rating.copy(foreUdderAttachment = it)) } }
|
||||
item { RatingScale(R.string.label_rear_udder_height, rating.rearUdderHeight, 10) { viewModel.onRatingChange(rating.copy(rearUdderHeight = it)) } }
|
||||
item { RatingScale(R.string.label_central_ligament, rating.centralLigament, 10) { viewModel.onRatingChange(rating.copy(centralLigament = it)) } }
|
||||
item { RatingScale(R.string.label_udder_depth, rating.udderDepth, 10) { viewModel.onRatingChange(rating.copy(udderDepth = it)) } }
|
||||
item { RatingScale(R.string.label_front_teat_position, rating.frontTeatPosition, 10) { viewModel.onRatingChange(rating.copy(frontTeatPosition = it)) } }
|
||||
item { RatingScale(R.string.label_teat_length, rating.teatLength, 10) { viewModel.onRatingChange(rating.copy(teatLength = it)) } }
|
||||
item { RatingScale(R.string.label_rear_teat_position, rating.rearTeatPosition, 10) { viewModel.onRatingChange(rating.copy(rearTeatPosition = it)) } }
|
||||
item { RatingScale(R.string.label_rear_udder_width, rating.rearUdderWidth, 10) { viewModel.onRatingChange(rating.copy(rearUdderWidth = it)) } }
|
||||
item { RatingScale(R.string.label_teat_thickness, rating.teatThickness, 10) { viewModel.onRatingChange(rating.copy(teatThickness = it)) } }
|
||||
|
||||
// Other Attributes
|
||||
item { RatingScale(R.string.label_locomotion, rating.locomotion, 10) { viewModel.onRatingChange(rating.copy(locomotion = it)) } }
|
||||
item { RatingScale(R.string.label_body_condition_score, rating.bodyConditionScore, 10) { viewModel.onRatingChange(rating.copy(bodyConditionScore = it)) } }
|
||||
item { RatingScale(R.string.label_hock_development, rating.hockDevelopment, 10) { viewModel.onRatingChange(rating.copy(hockDevelopment = it)) } }
|
||||
item { RatingScale(R.string.label_bone_structure, rating.boneStructure, 10) { viewModel.onRatingChange(rating.copy(boneStructure = it)) } }
|
||||
item { RatingScale(R.string.label_muscularity, rating.muscularity, 10) { viewModel.onRatingChange(rating.copy(muscularity = it)) } }
|
||||
|
||||
// 4. Buttons
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = Dimentions.MEDIUM_PADDING),
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimentions.MEDIUM_PADDING)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { navController?.popBackStack() },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.btn_cancel))
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.saveRatings()
|
||||
navController?.popBackStack()
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.btn_save_ratings))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(Dimentions.LARGE_PADDING))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.example.livingai.pages.ratings
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.livingai.domain.model.AnimalRating
|
||||
import com.example.livingai.domain.usecases.GetAnimalDetails
|
||||
import com.example.livingai.domain.usecases.GetAnimalRatings
|
||||
import com.example.livingai.domain.usecases.SetAnimalRatings
|
||||
import com.example.livingai.pages.ratings.util.provideEmptyAnimalRating
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RatingViewModel(
|
||||
private val getAnimalDetails: GetAnimalDetails,
|
||||
private val getAnimalRatings: GetAnimalRatings,
|
||||
private val setAnimalRatings: SetAnimalRatings,
|
||||
private val savedStateHandle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
|
||||
private val _ratingState = MutableStateFlow<AnimalRating?>(null)
|
||||
val ratingState = _ratingState.asStateFlow()
|
||||
|
||||
private val _animalImages = MutableStateFlow<List<String>>(emptyList())
|
||||
val animalImages = _animalImages.asStateFlow()
|
||||
|
||||
private val animalId: String = savedStateHandle.get<String>("animalId")!!
|
||||
|
||||
init {
|
||||
loadAnimalDetails()
|
||||
loadAnimalRatings()
|
||||
}
|
||||
|
||||
private fun loadAnimalDetails() {
|
||||
getAnimalDetails(animalId).onEach {
|
||||
_animalImages.value = it?.images ?: emptyList()
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun loadAnimalRatings() {
|
||||
getAnimalRatings(animalId).onEach {
|
||||
_ratingState.value = it ?: provideEmptyAnimalRating(animalId)
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun onRatingChange(newRating: AnimalRating) {
|
||||
_ratingState.value = newRating
|
||||
}
|
||||
|
||||
fun saveRatings() {
|
||||
viewModelScope.launch {
|
||||
_ratingState.value?.let {
|
||||
setAnimalRatings(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.example.livingai.pages.ratings.util
|
||||
|
||||
import com.example.livingai.domain.model.AnimalRating
|
||||
|
||||
fun provideEmptyAnimalRating(animalId: String) = AnimalRating(
|
||||
animalId = animalId,
|
||||
overallRating = 0,
|
||||
healthRating = 0,
|
||||
breedRating = 0,
|
||||
stature = 0,
|
||||
chestWidth = 0,
|
||||
bodyDepth = 0,
|
||||
angularity = 0,
|
||||
rumpAngle = 0,
|
||||
rumpWidth = 0,
|
||||
rearLegSet = 0,
|
||||
rearLegRearView = 0,
|
||||
footAngle = 0,
|
||||
foreUdderAttachment = 0,
|
||||
rearUdderHeight = 0,
|
||||
centralLigament = 0,
|
||||
udderDepth = 0,
|
||||
frontTeatPosition = 0,
|
||||
teatLength = 0,
|
||||
rearTeatPosition = 0,
|
||||
locomotion = 0,
|
||||
bodyConditionScore = 0,
|
||||
hockDevelopment = 0,
|
||||
boneStructure = 0,
|
||||
rearUdderWidth = 0,
|
||||
teatThickness = 0,
|
||||
muscularity = 0,
|
||||
bodyConditionComments = ""
|
||||
)
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.example.livingai.pages.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
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.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavController
|
||||
import com.example.livingai.R
|
||||
import com.example.livingai.pages.commons.Dimentions
|
||||
import com.example.livingai.pages.components.CommonScaffold
|
||||
import com.example.livingai.pages.components.RadioGroup
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
navController: NavController,
|
||||
viewModel: SettingsViewModel = koinViewModel()
|
||||
) {
|
||||
val settings by viewModel.settings.collectAsState()
|
||||
val languageEntries = stringArrayResource(id = R.array.language_entries)
|
||||
val languageValues = stringArrayResource(id = R.array.language_values)
|
||||
val languageMap = languageEntries.zip(languageValues).toMap()
|
||||
|
||||
// Find the display name for the currently selected language value
|
||||
val selectedLanguageEntry = languageMap.entries.find { it.value == settings.language }?.key ?: languageEntries.first()
|
||||
|
||||
CommonScaffold(
|
||||
navController = navController,
|
||||
title = stringResource(id = R.string.top_bar_settings)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
.padding(Dimentions.SMALL_PADDING_TEXT)
|
||||
) {
|
||||
RadioGroup(
|
||||
titleRes = R.string.language,
|
||||
options = languageEntries.toList(),
|
||||
selected = selectedLanguageEntry,
|
||||
onSelected = { selectedEntry ->
|
||||
val selectedValue = languageMap[selectedEntry] ?: languageValues.first()
|
||||
viewModel.saveSettings(settings.copy(language = selectedValue))
|
||||
}
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = Dimentions.SMALL_PADDING_TEXT),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.auto_capture))
|
||||
Switch(
|
||||
checked = settings.isAutoCaptureOn,
|
||||
onCheckedChange = { isChecked ->
|
||||
viewModel.saveSettings(settings.copy(isAutoCaptureOn = isChecked))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.example.livingai.pages.settings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.livingai.data.local.model.SettingsData
|
||||
import com.example.livingai.domain.repository.SettingsRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SettingsViewModel(private val settingsRepository: SettingsRepository) : ViewModel() {
|
||||
|
||||
private val _settings = MutableStateFlow(SettingsData("en", false))
|
||||
val settings = _settings.asStateFlow()
|
||||
|
||||
init {
|
||||
getSettings()
|
||||
}
|
||||
|
||||
private fun getSettings() {
|
||||
settingsRepository.getSettings().onEach {
|
||||
_settings.value = it
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun saveSettings(settings: SettingsData) {
|
||||
viewModelScope.launch {
|
||||
settingsRepository.saveSettings(settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package com.example.livingai.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
// Nature Theme Colors
|
||||
val md_theme_light_primary = Color(0xFF2E7D32)
|
||||
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_primaryContainer = Color(0xFFB9F6CA)
|
||||
val md_theme_light_onPrimaryContainer = Color(0xFF00210B)
|
||||
val md_theme_light_secondary = Color(0xFF558B2F)
|
||||
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_secondaryContainer = Color(0xFFDCEDC8)
|
||||
val md_theme_light_onSecondaryContainer = Color(0xFF131F0A)
|
||||
val md_theme_light_tertiary = Color(0xFF00838F)
|
||||
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_tertiaryContainer = Color(0xFFB2EBF2)
|
||||
val md_theme_light_onTertiaryContainer = Color(0xFF001F24)
|
||||
val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
val md_theme_light_background = Color(0xFFF1F8E9)
|
||||
val md_theme_light_onBackground = Color(0xFF1A1C19)
|
||||
val md_theme_light_surface = Color(0xFFF1F8E9)
|
||||
val md_theme_light_onSurface = Color(0xFF1A1C19)
|
||||
val md_theme_light_surfaceVariant = Color(0xFFDDE5DB)
|
||||
val md_theme_light_onSurfaceVariant = Color(0xFF414942)
|
||||
val md_theme_light_outline = Color(0xFF717971)
|
||||
val md_theme_light_inverseOnSurface = Color(0xFFF0F1EB)
|
||||
val md_theme_light_inverseSurface = Color(0xFF2F312D)
|
||||
val md_theme_light_inversePrimary = Color(0xFF66BB6A)
|
||||
val md_theme_light_shadow = Color(0xFF000000)
|
||||
val md_theme_light_surfaceTint = Color(0xFF2E7D32)
|
||||
val md_theme_light_outlineVariant = Color(0xFFC1C9BF)
|
||||
val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
val md_theme_dark_primary = Color(0xFF66BB6A)
|
||||
val md_theme_dark_onPrimary = Color(0xFF003916)
|
||||
val md_theme_dark_primaryContainer = Color(0xFF005223)
|
||||
val md_theme_dark_onPrimaryContainer = Color(0xFFB9F6CA)
|
||||
val md_theme_dark_secondary = Color(0xFFAED581)
|
||||
val md_theme_dark_onSecondary = Color(0xFF25351A)
|
||||
val md_theme_dark_secondaryContainer = Color(0xFF3C4D31)
|
||||
val md_theme_dark_onSecondaryContainer = Color(0xFFDCEDC8)
|
||||
val md_theme_dark_tertiary = Color(0xFF4DD0E1)
|
||||
val md_theme_dark_onTertiary = Color(0xFF00363D)
|
||||
val md_theme_dark_tertiaryContainer = Color(0xFF004F58)
|
||||
val md_theme_dark_onTertiaryContainer = Color(0xFFB2EBF2)
|
||||
val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
val md_theme_dark_onError = Color(0xFF690005)
|
||||
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
val md_theme_dark_background = Color(0xFF1A1C19)
|
||||
val md_theme_dark_onBackground = Color(0xFFE2E3DD)
|
||||
val md_theme_dark_surface = Color(0xFF1A1C19)
|
||||
val md_theme_dark_onSurface = Color(0xFFE2E3DD)
|
||||
val md_theme_dark_surfaceVariant = Color(0xFF414942)
|
||||
val md_theme_dark_onSurfaceVariant = Color(0xFFC1C9BF)
|
||||
val md_theme_dark_outline = Color(0xFF8B938A)
|
||||
val md_theme_dark_inverseOnSurface = Color(0xFF1A1C19)
|
||||
val md_theme_dark_inverseSurface = Color(0xFFE2E3DD)
|
||||
val md_theme_dark_inversePrimary = Color(0xFF2E7D32)
|
||||
val md_theme_dark_shadow = Color(0xFF000000)
|
||||
val md_theme_dark_surfaceTint = Color(0xFF66BB6A)
|
||||
val md_theme_dark_outlineVariant = Color(0xFF414942)
|
||||
val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
|
||||
// Compatibility with older definitions
|
||||
val BlueGray = Color(0xFFA0A3BD)
|
||||
val WhiteGray = Color(0xFFB0B3B8)
|
||||
|
||||
val primaryCol = md_theme_light_primary
|
||||
val secondaryCol = md_theme_light_secondary
|
||||
val alternateCol = md_theme_light_tertiary
|
||||
val on_primaryCol = md_theme_light_onPrimary
|
||||
val on_secondaryCol = md_theme_light_onSecondary
|
||||
val on_alternateCol = md_theme_light_onTertiary
|
||||
val primary_contentCol = md_theme_light_onPrimary
|
||||
val secondary_contentCol = md_theme_light_onSecondary
|
||||
val alternate_contentCol = md_theme_light_onTertiary
|
||||
val primary_containerCol = md_theme_light_primaryContainer
|
||||
val secondary_containerCol = md_theme_light_secondaryContainer
|
||||
val alternate_containerCol = md_theme_light_tertiaryContainer
|
||||
val primary_container_contentCol = md_theme_light_onPrimaryContainer
|
||||
val secondary_container_contentCol = md_theme_light_onSecondaryContainer
|
||||
val alternate_container_contentCol = md_theme_light_onTertiaryContainer
|
||||
val primary_backgroundCol = md_theme_light_background
|
||||
val secondary_backgroundCol = md_theme_light_surface
|
||||
val alternate_backgroundCol = md_theme_light_surfaceVariant
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.example.livingai.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LivingAITheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = false,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
window.statusBarColor = colorScheme.primary.toArgb()
|
||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue