commit e0d55e3087fea98a2379784bf27f45fa90ab945f Author: SaiD Date: Thu Dec 4 09:42:35 2025 +0530 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7061a0d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/studiobot.xml b/.idea/studiobot.xml new file mode 100644 index 0000000..539e3b8 --- /dev/null +++ b/.idea/studiobot.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.kotlin/errors/errors-1764699844919.log b/.kotlin/errors/errors-1764699844919.log new file mode 100644 index 0000000..96041b8 Binary files /dev/null and b/.kotlin/errors/errors-1764699844919.log differ diff --git a/.kotlin/errors/errors-1764819290198.log b/.kotlin/errors/errors-1764819290198.log new file mode 100644 index 0000000..1219b50 --- /dev/null +++ b/.kotlin/errors/errors-1764819290198.log @@ -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 + diff --git a/.kotlin/sessions/kotlin-compiler-1933245961721594071.salive b/.kotlin/sessions/kotlin-compiler-1933245961721594071.salive new file mode 100644 index 0000000..e69de29 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..5938e78 --- /dev/null +++ b/app/build.gradle.kts @@ -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) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/livingai/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/livingai/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..23b80ee --- /dev/null +++ b/app/src/androidTest/java/com/example/livingai/ExampleInstrumentedTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..800685e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/LivingAIApplication.kt b/app/src/main/java/com/example/livingai/LivingAIApplication.kt new file mode 100644 index 0000000..3817c13 --- /dev/null +++ b/app/src/main/java/com/example/livingai/LivingAIApplication.kt @@ -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) + } + } +} diff --git a/app/src/main/java/com/example/livingai/MainActivity.kt b/app/src/main/java/com/example/livingai/MainActivity.kt new file mode 100644 index 0000000..f992230 --- /dev/null +++ b/app/src/main/java/com/example/livingai/MainActivity.kt @@ -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() + + 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) + } + } + } + } +} diff --git a/app/src/main/java/com/example/livingai/data/local/AnimalDataPagingSource.kt b/app/src/main/java/com/example/livingai/data/local/AnimalDataPagingSource.kt new file mode 100644 index 0000000..24eea78 --- /dev/null +++ b/app/src/main/java/com/example/livingai/data/local/AnimalDataPagingSource.kt @@ -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() { + + override fun getRefreshKey(state: PagingState): 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): LoadResult { + 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) + } + } +} diff --git a/app/src/main/java/com/example/livingai/data/local/CSVDataSource.kt b/app/src/main/java/com/example/livingai/data/local/CSVDataSource.kt new file mode 100644 index 0000000..69ac26e --- /dev/null +++ b/app/src/main/java/com/example/livingai/data/local/CSVDataSource.kt @@ -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> = 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>) = 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> = + Pager(PagingConfig(pageSize = 20)) { + AnimalDataPagingSource(this) + }.flow + + suspend fun getAllAnimalProfiles(): List { + return readAllLines().mapNotNull(::parseAnimalProfile) + } + + override fun getAnimalDetails(animalId: String): Flow = flow { + emit(parseAnimalDetails(readAllLines().find { it.getOrNull(INDEX_ID) == animalId })) + }.flowOn(Dispatchers.IO) + + override fun getAnimalRatings(animalId: String): Flow = 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 = Array(TOTAL_COLUMNS) { "" } + + private fun parseAnimalProfile(row: Array?): 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?): 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?): 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, p: AnimalProfile): Array { + 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, d: AnimalDetails): Array { + 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, r: AnimalRating): Array { + 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" + ) + } +} diff --git a/app/src/main/java/com/example/livingai/data/local/model/SettingsData.kt b/app/src/main/java/com/example/livingai/data/local/model/SettingsData.kt new file mode 100644 index 0000000..0290ba6 --- /dev/null +++ b/app/src/main/java/com/example/livingai/data/local/model/SettingsData.kt @@ -0,0 +1,6 @@ +package com.example.livingai.data.local.model + +data class SettingsData( + val language: String, + val isAutoCaptureOn: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/data/manager/LocalUserManagerImpl.kt b/app/src/main/java/com/example/livingai/data/manager/LocalUserManagerImpl.kt new file mode 100644 index 0000000..a3e0431 --- /dev/null +++ b/app/src/main/java/com/example/livingai/data/manager/LocalUserManagerImpl.kt @@ -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 +): LocalUserManager { + override suspend fun saveAppEntry() { + dataStore.edit { settings -> + settings[PreferencesKeys.APP_ENTRY] = true + } + } + + override fun readAppEntry(): Flow { + return dataStore.data.map { preferences -> + preferences[PreferencesKeys.APP_ENTRY] ?: false + } + } + +} + +private object PreferencesKeys { + val APP_ENTRY = booleanPreferencesKey(name = Constants.APP_ENTRY) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/data/ml/AIModelImpl.kt b/app/src/main/java/com/example/livingai/data/ml/AIModelImpl.kt new file mode 100644 index 0000000..1f9fdbf --- /dev/null +++ b/app/src/main/java/com/example/livingai/data/ml/AIModelImpl.kt @@ -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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/data/repository/SettingsRepositoryImpl.kt b/app/src/main/java/com/example/livingai/data/repository/SettingsRepositoryImpl.kt new file mode 100644 index 0000000..c602060 --- /dev/null +++ b/app/src/main/java/com/example/livingai/data/repository/SettingsRepositoryImpl.kt @@ -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) : SettingsRepository { + + private object PreferencesKeys { + val LANGUAGE = stringPreferencesKey("language") + val IS_AUTO_CAPTURE_ON = booleanPreferencesKey("is_auto_capture_on") + } + + override fun getSettings(): Flow { + 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/data/repository/business/AnimalDetailsRepositoryImpl.kt b/app/src/main/java/com/example/livingai/data/repository/business/AnimalDetailsRepositoryImpl.kt new file mode 100644 index 0000000..3d0243b --- /dev/null +++ b/app/src/main/java/com/example/livingai/data/repository/business/AnimalDetailsRepositoryImpl.kt @@ -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 { + 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: + } +} diff --git a/app/src/main/java/com/example/livingai/data/repository/business/AnimalProfileRepositoryImpl.kt b/app/src/main/java/com/example/livingai/data/repository/business/AnimalProfileRepositoryImpl.kt new file mode 100644 index 0000000..8ef1d82 --- /dev/null +++ b/app/src/main/java/com/example/livingai/data/repository/business/AnimalProfileRepositoryImpl.kt @@ -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> { + return dataSource.getAnimalProfiles() + } + + override suspend fun saveAnimalProfile(animalProfile: AnimalProfile) { + dataSource.setAnimalProfile(animalProfile) + } + + override suspend fun deleteAnimalProfile(id: String) { + dataSource.deleteAnimalProfile(id) + } +} diff --git a/app/src/main/java/com/example/livingai/data/repository/business/AnimalRatingRepositoryImpl.kt b/app/src/main/java/com/example/livingai/data/repository/business/AnimalRatingRepositoryImpl.kt new file mode 100644 index 0000000..d874f6a --- /dev/null +++ b/app/src/main/java/com/example/livingai/data/repository/business/AnimalRatingRepositoryImpl.kt @@ -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 { + 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 + } +} diff --git a/app/src/main/java/com/example/livingai/data/repository/media/CameraRepositoryImpl.kt b/app/src/main/java/com/example/livingai/data/repository/media/CameraRepositoryImpl.kt new file mode 100644 index 0000000..0763b38 --- /dev/null +++ b/app/src/main/java/com/example/livingai/data/repository/media/CameraRepositoryImpl.kt @@ -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() + } +} diff --git a/app/src/main/java/com/example/livingai/data/repository/media/VideoRepositoryImpl.kt b/app/src/main/java/com/example/livingai/data/repository/media/VideoRepositoryImpl.kt new file mode 100644 index 0000000..b5d427b --- /dev/null +++ b/app/src/main/java/com/example/livingai/data/repository/media/VideoRepositoryImpl.kt @@ -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 { + "" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/di/AppModule.kt b/app/src/main/java/com/example/livingai/di/AppModule.kt new file mode 100644 index 0000000..ff63050 --- /dev/null +++ b/app/src/main/java/com/example/livingai/di/AppModule.kt @@ -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 by preferencesDataStore(name = Constants.USER_SETTINGS) + +val appModule = module { + single { LocalUserManagerImpl(get()) } + single> { androidContext().dataStore } + + single { + AppEntryUseCases( + readAppEntry = ReadAppEntry(get()), + saveAppEntry = SaveAppEntry(get()) + ) + } + + // Data Source + single { + CSVDataSource( + context = androidContext(), + fileName = Constants.ANIMAL_DATA_FILENAME + ) + } + + // ML Model + single { AIModelImpl() } + + // Repositories + single { AnimalProfileRepositoryImpl(get()) } + single { AnimalDetailsRepositoryImpl(get()) } + single { AnimalRatingRepositoryImpl(get()) } + single { SettingsRepositoryImpl(get()) } + single { CameraRepositoryImpl(get(), androidContext()) } + single { 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() } +} diff --git a/app/src/main/java/com/example/livingai/domain/manager/LocalUserManager.kt b/app/src/main/java/com/example/livingai/domain/manager/LocalUserManager.kt new file mode 100644 index 0000000..6cb9ae6 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/manager/LocalUserManager.kt @@ -0,0 +1,8 @@ +package com.example.livingai.domain.manager + +import kotlinx.coroutines.flow.Flow + +interface LocalUserManager { + suspend fun saveAppEntry() + fun readAppEntry(): Flow +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/domain/ml/AIModel.kt b/app/src/main/java/com/example/livingai/domain/ml/AIModel.kt new file mode 100644 index 0000000..5c1f336 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/ml/AIModel.kt @@ -0,0 +1,7 @@ +package com.example.livingai.domain.ml + +import android.graphics.Bitmap + +interface AIModel { + fun deriveInference(bitmap: Bitmap): String +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/domain/model/AnimalDetails.kt b/app/src/main/java/com/example/livingai/domain/model/AnimalDetails.kt new file mode 100644 index 0000000..95a4031 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/model/AnimalDetails.kt @@ -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, + val video: String +) diff --git a/app/src/main/java/com/example/livingai/domain/model/AnimalProfile.kt b/app/src/main/java/com/example/livingai/domain/model/AnimalProfile.kt new file mode 100644 index 0000000..bcaa356 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/model/AnimalProfile.kt @@ -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 +) diff --git a/app/src/main/java/com/example/livingai/domain/model/AnimalRating.kt b/app/src/main/java/com/example/livingai/domain/model/AnimalRating.kt new file mode 100644 index 0000000..0fe3137 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/model/AnimalRating.kt @@ -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, +) diff --git a/app/src/main/java/com/example/livingai/domain/model/SettingsData.kt b/app/src/main/java/com/example/livingai/domain/model/SettingsData.kt new file mode 100644 index 0000000..6ecad9b --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/model/SettingsData.kt @@ -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 +) diff --git a/app/src/main/java/com/example/livingai/domain/repository/CameraRepository.kt b/app/src/main/java/com/example/livingai/domain/repository/CameraRepository.kt new file mode 100644 index 0000000..11f1a3e --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/repository/CameraRepository.kt @@ -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 +} diff --git a/app/src/main/java/com/example/livingai/domain/repository/SettingsRepository.kt b/app/src/main/java/com/example/livingai/domain/repository/SettingsRepository.kt new file mode 100644 index 0000000..e2d47d4 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/repository/SettingsRepository.kt @@ -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 + suspend fun saveSettings(settings: SettingsData) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/domain/repository/VideoRepository.kt b/app/src/main/java/com/example/livingai/domain/repository/VideoRepository.kt new file mode 100644 index 0000000..4a48da4 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/repository/VideoRepository.kt @@ -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 +} diff --git a/app/src/main/java/com/example/livingai/domain/repository/business/AnimalDetailsRepository.kt b/app/src/main/java/com/example/livingai/domain/repository/business/AnimalDetailsRepository.kt new file mode 100644 index 0000000..69ca2b3 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/repository/business/AnimalDetailsRepository.kt @@ -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 + + suspend fun saveAnimalDetails(animalDetails: AnimalDetails) + + suspend fun deleteAnimalDetails(id: String) +} diff --git a/app/src/main/java/com/example/livingai/domain/repository/business/AnimalProfileRepository.kt b/app/src/main/java/com/example/livingai/domain/repository/business/AnimalProfileRepository.kt new file mode 100644 index 0000000..09d388c --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/repository/business/AnimalProfileRepository.kt @@ -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> + + suspend fun saveAnimalProfile(animalProfile: AnimalProfile) + + suspend fun deleteAnimalProfile(id: String) +} diff --git a/app/src/main/java/com/example/livingai/domain/repository/business/AnimalRatingRepository.kt b/app/src/main/java/com/example/livingai/domain/repository/business/AnimalRatingRepository.kt new file mode 100644 index 0000000..2b06dcb --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/repository/business/AnimalRatingRepository.kt @@ -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 + + suspend fun saveAnimalRating(animalRating: AnimalRating) + + suspend fun deleteAnimalRating(id: String) +} diff --git a/app/src/main/java/com/example/livingai/domain/repository/business/DataSource.kt b/app/src/main/java/com/example/livingai/domain/repository/business/DataSource.kt new file mode 100644 index 0000000..f5cbac9 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/repository/business/DataSource.kt @@ -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> + fun getAnimalDetails(animalId: String): Flow + fun getAnimalRatings(animalId: String): Flow + + suspend fun setAnimalProfile(animalProfile: AnimalProfile) + suspend fun setAnimalDetails(animalDetails: AnimalDetails) + suspend fun setAnimalRatings(animalRating: AnimalRating) + suspend fun deleteAnimalProfile(animalId: String) +} diff --git a/app/src/main/java/com/example/livingai/domain/usecases/AppEntry/AppEntryUseCases.kt b/app/src/main/java/com/example/livingai/domain/usecases/AppEntry/AppEntryUseCases.kt new file mode 100644 index 0000000..f868370 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/AppEntry/AppEntryUseCases.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/domain/usecases/DeleteAnimalProfile.kt b/app/src/main/java/com/example/livingai/domain/usecases/DeleteAnimalProfile.kt new file mode 100644 index 0000000..c9bb115 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/DeleteAnimalProfile.kt @@ -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) + } +} diff --git a/app/src/main/java/com/example/livingai/domain/usecases/GetAnimalDetails.kt b/app/src/main/java/com/example/livingai/domain/usecases/GetAnimalDetails.kt new file mode 100644 index 0000000..d24b038 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/GetAnimalDetails.kt @@ -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 { + return animalDetailsRepository.getAnimalDetails(animalId) + } +} diff --git a/app/src/main/java/com/example/livingai/domain/usecases/GetAnimalProfiles.kt b/app/src/main/java/com/example/livingai/domain/usecases/GetAnimalProfiles.kt new file mode 100644 index 0000000..c43fa26 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/GetAnimalProfiles.kt @@ -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> { + return animalProfileRepository.getAnimalProfiles() + } +} diff --git a/app/src/main/java/com/example/livingai/domain/usecases/GetAnimalRatings.kt b/app/src/main/java/com/example/livingai/domain/usecases/GetAnimalRatings.kt new file mode 100644 index 0000000..e4fde23 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/GetAnimalRatings.kt @@ -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 { + return animalRatingRepository.getAnimalRating(animalId) + } +} diff --git a/app/src/main/java/com/example/livingai/domain/usecases/ProfileEntry/ProfileEntryUseCase.kt b/app/src/main/java/com/example/livingai/domain/usecases/ProfileEntry/ProfileEntryUseCase.kt new file mode 100644 index 0000000..aeda5b4 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/ProfileEntry/ProfileEntryUseCase.kt @@ -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, +) diff --git a/app/src/main/java/com/example/livingai/domain/usecases/ProfileListing/ProfileListingUseCase.kt b/app/src/main/java/com/example/livingai/domain/usecases/ProfileListing/ProfileListingUseCase.kt new file mode 100644 index 0000000..95f8099 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/ProfileListing/ProfileListingUseCase.kt @@ -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 +) diff --git a/app/src/main/java/com/example/livingai/domain/usecases/ProfilesEntry/ProfilesEntryUseCases.kt b/app/src/main/java/com/example/livingai/domain/usecases/ProfilesEntry/ProfilesEntryUseCases.kt new file mode 100644 index 0000000..c69abeb --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/ProfilesEntry/ProfilesEntryUseCases.kt @@ -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 +) diff --git a/app/src/main/java/com/example/livingai/domain/usecases/ReadAppEntry.kt b/app/src/main/java/com/example/livingai/domain/usecases/ReadAppEntry.kt new file mode 100644 index 0000000..c74644d --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/ReadAppEntry.kt @@ -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 { + return localUserManager.readAppEntry() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/domain/usecases/SaveAppEntry.kt b/app/src/main/java/com/example/livingai/domain/usecases/SaveAppEntry.kt new file mode 100644 index 0000000..a6cf321 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/SaveAppEntry.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/domain/usecases/SetAnimalDetails.kt b/app/src/main/java/com/example/livingai/domain/usecases/SetAnimalDetails.kt new file mode 100644 index 0000000..0e6eebd --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/SetAnimalDetails.kt @@ -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) + } +} diff --git a/app/src/main/java/com/example/livingai/domain/usecases/SetAnimalRatings.kt b/app/src/main/java/com/example/livingai/domain/usecases/SetAnimalRatings.kt new file mode 100644 index 0000000..1df9128 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/SetAnimalRatings.kt @@ -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) + } +} diff --git a/app/src/main/java/com/example/livingai/pages/addprofile/AddProfileScreen.kt b/app/src/main/java/com/example/livingai/pages/addprofile/AddProfileScreen.kt new file mode 100644 index 0000000..da8409f --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/addprofile/AddProfileScreen.kt @@ -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)) + } + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/addprofile/AddProfileViewModel.kt b/app/src/main/java/com/example/livingai/pages/addprofile/AddProfileViewModel.kt new file mode 100644 index 0000000..d5ebed8 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/addprofile/AddProfileViewModel.kt @@ -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(null) + val animalDetails: State = _animalDetails + + private val _currentAnimalId = mutableStateOf(null) + val currentAnimalId: State = _currentAnimalId + + // UI State + var species = mutableStateOf(null) + var breed = mutableStateOf(null) + var age = mutableStateOf("") + var milkYield = mutableStateOf("") + var calvingNumber = mutableStateOf("") + var reproductiveStatus = mutableStateOf(null) + var description = mutableStateOf("") + + // State for photos and video + val photos = mutableStateMapOf() + private val _videoUri = mutableStateOf(null) + val videoUri: State = _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) + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/camera/CameraScreen.kt b/app/src/main/java/com/example/livingai/pages/camera/CameraScreen.kt new file mode 100644 index 0000000..a5ecead --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/camera/CameraScreen.kt @@ -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) } + ) + } + } + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/camera/CameraViewModel.kt b/app/src/main/java/com/example/livingai/pages/camera/CameraViewModel.kt new file mode 100644 index 0000000..9f84f94 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/camera/CameraViewModel.kt @@ -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() +} diff --git a/app/src/main/java/com/example/livingai/pages/camera/VideoRecordScreen.kt b/app/src/main/java/com/example/livingai/pages/camera/VideoRecordScreen.kt new file mode 100644 index 0000000..eb18e58 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/camera/VideoRecordScreen.kt @@ -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(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 = {} + ) + } + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/camera/VideoViewModel.kt b/app/src/main/java/com/example/livingai/pages/camera/VideoViewModel.kt new file mode 100644 index 0000000..8fab93e --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/camera/VideoViewModel.kt @@ -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() +} diff --git a/app/src/main/java/com/example/livingai/pages/camera/ViewImageScreen.kt b/app/src/main/java/com/example/livingai/pages/camera/ViewImageScreen.kt new file mode 100644 index 0000000..9d1b257 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/camera/ViewImageScreen.kt @@ -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") + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/camera/ViewVideoScreen.kt b/app/src/main/java/com/example/livingai/pages/camera/ViewVideoScreen.kt new file mode 100644 index 0000000..b78e93d --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/camera/ViewVideoScreen.kt @@ -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() + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/commons/Dimentions.kt b/app/src/main/java/com/example/livingai/pages/commons/Dimentions.kt new file mode 100644 index 0000000..2aa8b12 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/commons/Dimentions.kt @@ -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 +} diff --git a/app/src/main/java/com/example/livingai/pages/components/AnimalProfileCard.kt b/app/src/main/java/com/example/livingai/pages/components/AnimalProfileCard.kt new file mode 100644 index 0000000..d3c8872 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/components/AnimalProfileCard.kt @@ -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)) + } + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/components/CameraPreview.kt b/app/src/main/java/com/example/livingai/pages/components/CameraPreview.kt new file mode 100644 index 0000000..af6b568 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/components/CameraPreview.kt @@ -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 + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/pages/components/CommonScaffold.kt b/app/src/main/java/com/example/livingai/pages/components/CommonScaffold.kt new file mode 100644 index 0000000..1a83277 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/components/CommonScaffold.kt @@ -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) + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/components/FlexButton.kt b/app/src/main/java/com/example/livingai/pages/components/FlexButton.kt new file mode 100644 index 0000000..ea749b9 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/components/FlexButton.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/pages/components/ImageThumbnailButton.kt b/app/src/main/java/com/example/livingai/pages/components/ImageThumbnailButton.kt new file mode 100644 index 0000000..274f135 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/components/ImageThumbnailButton.kt @@ -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) + ) + } + } + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/components/LabeledDropdown.kt b/app/src/main/java/com/example/livingai/pages/components/LabeledDropdown.kt new file mode 100644 index 0000000..bc3bd75 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/components/LabeledDropdown.kt @@ -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, + 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 + } + ) + } + } + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/components/LabeledTextField.kt b/app/src/main/java/com/example/livingai/pages/components/LabeledTextField.kt new file mode 100644 index 0000000..d01acb5 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/components/LabeledTextField.kt @@ -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) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/pages/components/LivingAIBottomBar.kt b/app/src/main/java/com/example/livingai/pages/components/LivingAIBottomBar.kt new file mode 100644 index 0000000..53f827e --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/components/LivingAIBottomBar.kt @@ -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, + 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) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/pages/components/OnBoardingPage.kt b/app/src/main/java/com/example/livingai/pages/components/OnBoardingPage.kt new file mode 100644 index 0000000..6acaedb --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/components/OnBoardingPage.kt @@ -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] + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/pages/components/PageIndicator.kt b/app/src/main/java/com/example/livingai/pages/components/PageIndicator.kt new file mode 100644 index 0000000..06b3fc8 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/components/PageIndicator.kt @@ -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) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/pages/components/PermissionWrapper.kt b/app/src/main/java/com/example/livingai/pages/components/PermissionWrapper.kt new file mode 100644 index 0000000..97fcb47 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/components/PermissionWrapper.kt @@ -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 = 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") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/pages/components/RadioGroup.kt b/app/src/main/java/com/example/livingai/pages/components/RadioGroup.kt new file mode 100644 index 0000000..d4bb633 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/components/RadioGroup.kt @@ -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, + 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 + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/pages/components/RatingScale.kt b/app/src/main/java/com/example/livingai/pages/components/RatingScale.kt new file mode 100644 index 0000000..d8a0327 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/components/RatingScale.kt @@ -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 + ) + } + } + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/components/ThumbnailImage.kt b/app/src/main/java/com/example/livingai/pages/components/ThumbnailImage.kt new file mode 100644 index 0000000..3436ce9 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/components/ThumbnailImage.kt @@ -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 + ) +} diff --git a/app/src/main/java/com/example/livingai/pages/components/VideoThumbnailButton.kt b/app/src/main/java/com/example/livingai/pages/components/VideoThumbnailButton.kt new file mode 100644 index 0000000..67f4b34 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/components/VideoThumbnailButton.kt @@ -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) + ) + } + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/home/HomeScreen.kt b/app/src/main/java/com/example/livingai/pages/home/HomeScreen.kt new file mode 100644 index 0000000..ab60202 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/home/HomeScreen.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/pages/home/HomeViewModel.kt b/app/src/main/java/com/example/livingai/pages/home/HomeViewModel.kt new file mode 100644 index 0000000..f065727 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/home/HomeViewModel.kt @@ -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 = _splashCondition + + private val _startDestination = mutableStateOf(Route.AppStartNavigation) + val startDestination: State = _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) + } +} diff --git a/app/src/main/java/com/example/livingai/pages/listings/ListingsScreen.kt b/app/src/main/java/com/example/livingai/pages/listings/ListingsScreen.kt new file mode 100644 index 0000000..2e03230 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/listings/ListingsScreen.kt @@ -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 = 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) } + ) + } + } + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/listings/ListingsViewModel.kt b/app/src/main/java/com/example/livingai/pages/listings/ListingsViewModel.kt new file mode 100644 index 0000000..50c1a6a --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/listings/ListingsViewModel.kt @@ -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) + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/navigation/NavGraph.kt b/app/src/main/java/com/example/livingai/pages/navigation/NavGraph.kt new file mode 100644 index 0000000..10f5d04 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/navigation/NavGraph.kt @@ -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( + startDestination = Route.OnBoardingScreen + ) { + composable { + val onBoardingViewModel: OnBoardingViewModel = koinViewModel() + OnBoardingScreen( + events = onBoardingViewModel::onEvent + ) + } + } + + navigation( + startDestination = Route.HomeScreen + ) { + composable { + HomeScreen(navController = navController) + } + + composable { + ListingsScreen( + navController = navController + ) + } + + composable { 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("newImageUri") + val newImageOrientation = backStackEntry.savedStateHandle.get("newImageOrientation") + val newVideoUri = backStackEntry.savedStateHandle.get("newVideoUri") + + LaunchedEffect(newImageUri, newImageOrientation) { + if (newImageUri != null && newImageOrientation != null) { + viewModel.addPhoto(newImageOrientation, newImageUri) + backStackEntry.savedStateHandle.remove("newImageUri") + backStackEntry.savedStateHandle.remove("newImageOrientation") + } + } + LaunchedEffect(newVideoUri) { + if (newVideoUri != null) { + viewModel.setVideo(newVideoUri) + backStackEntry.savedStateHandle.remove("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 { + RatingScreen() + } + + composable { + SettingsScreen(navController = navController) + } + + composable { backStackEntry -> + val route: Route.CameraScreen = backStackEntry.toRoute() + CameraScreen(navController = navController, orientation = route.orientation, animalId = route.animalId) + } + + composable { backStackEntry -> + val route: Route.VideoRecordScreen = backStackEntry.toRoute() + VideoRecordScreen(navController = navController, animalId = route.animalId) + } + + composable { 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().savedStateHandle["newImageUri"] = args.imageUri + navController.getBackStackEntry().savedStateHandle["newImageOrientation"] = args.orientation + navController.popBackStack(inclusive = false) + }, + onBack = { navController.popBackStack() } + ) + } + + composable { 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().savedStateHandle["newVideoUri"] = args.videoUri + navController.popBackStack(inclusive = false) + }, + onBack = { navController.popBackStack() } + ) + } + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/navigation/Route.kt b/app/src/main/java/com/example/livingai/pages/navigation/Route.kt new file mode 100644 index 0000000..631eff8 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/navigation/Route.kt @@ -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() +} diff --git a/app/src/main/java/com/example/livingai/pages/onboarding/OnBoardingEvents.kt b/app/src/main/java/com/example/livingai/pages/onboarding/OnBoardingEvents.kt new file mode 100644 index 0000000..89bafd7 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/onboarding/OnBoardingEvents.kt @@ -0,0 +1,6 @@ +package com.example.livingai.pages.onboarding + +sealed class OnBoardingEvents { + object SaveAppEntry: OnBoardingEvents() + data class SaveLanguage(val language: String): OnBoardingEvents() +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/pages/onboarding/OnBoardingScreen.kt b/app/src/main/java/com/example/livingai/pages/onboarding/OnBoardingScreen.kt new file mode 100644 index 0000000..8b7deae --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/onboarding/OnBoardingScreen.kt @@ -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) + } + } + } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/pages/onboarding/OnBoardingViewModel.kt b/app/src/main/java/com/example/livingai/pages/onboarding/OnBoardingViewModel.kt new file mode 100644 index 0000000..10a1a79 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/onboarding/OnBoardingViewModel.kt @@ -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)) + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/onboarding/Page.kt b/app/src/main/java/com/example/livingai/pages/onboarding/Page.kt new file mode 100644 index 0000000..2b32785 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/onboarding/Page.kt @@ -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 + ) +) diff --git a/app/src/main/java/com/example/livingai/pages/ratings/RatingScreen.kt b/app/src/main/java/com/example/livingai/pages/ratings/RatingScreen.kt new file mode 100644 index 0000000..e5c6b2c --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/ratings/RatingScreen.kt @@ -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)) + } + } + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/ratings/RatingViewModel.kt b/app/src/main/java/com/example/livingai/pages/ratings/RatingViewModel.kt new file mode 100644 index 0000000..085b8b4 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/ratings/RatingViewModel.kt @@ -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(null) + val ratingState = _ratingState.asStateFlow() + + private val _animalImages = MutableStateFlow>(emptyList()) + val animalImages = _animalImages.asStateFlow() + + private val animalId: String = savedStateHandle.get("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) + } + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/ratings/util/EmptyAnimalRatingProvider.kt b/app/src/main/java/com/example/livingai/pages/ratings/util/EmptyAnimalRatingProvider.kt new file mode 100644 index 0000000..ccb6a90 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/ratings/util/EmptyAnimalRatingProvider.kt @@ -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 = "" +) diff --git a/app/src/main/java/com/example/livingai/pages/settings/SettingsScreen.kt b/app/src/main/java/com/example/livingai/pages/settings/SettingsScreen.kt new file mode 100644 index 0000000..a0b16a3 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/settings/SettingsScreen.kt @@ -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)) + } + ) + } + } + } +} diff --git a/app/src/main/java/com/example/livingai/pages/settings/SettingsViewModel.kt b/app/src/main/java/com/example/livingai/pages/settings/SettingsViewModel.kt new file mode 100644 index 0000000..d8999e7 --- /dev/null +++ b/app/src/main/java/com/example/livingai/pages/settings/SettingsViewModel.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/ui/theme/Color.kt b/app/src/main/java/com/example/livingai/ui/theme/Color.kt new file mode 100644 index 0000000..96b396d --- /dev/null +++ b/app/src/main/java/com/example/livingai/ui/theme/Color.kt @@ -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 diff --git a/app/src/main/java/com/example/livingai/ui/theme/Theme.kt b/app/src/main/java/com/example/livingai/ui/theme/Theme.kt new file mode 100644 index 0000000..df3dbef --- /dev/null +++ b/app/src/main/java/com/example/livingai/ui/theme/Theme.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/ui/theme/Type.kt b/app/src/main/java/com/example/livingai/ui/theme/Type.kt new file mode 100644 index 0000000..4b4b6b2 --- /dev/null +++ b/app/src/main/java/com/example/livingai/ui/theme/Type.kt @@ -0,0 +1,50 @@ +package com.example.livingai.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import com.example.livingai.R + +val Poppins = FontFamily( + fonts = listOf( + Font(R.font.poppins_regular, FontWeight.Normal), + Font(R.font.poppins_bold, FontWeight.Bold), + Font(R.font.poppins_semibold, FontWeight.SemiBold), + ) +) + +val Typography = Typography( + displaySmall = TextStyle( + fontSize = 24.sp, + fontFamily = Poppins, + fontWeight = FontWeight.Normal, + lineHeight = 36.sp, + ), + displayMedium = TextStyle( + fontSize = 32.sp, + fontFamily = Poppins, + fontWeight = FontWeight.Normal, + lineHeight = 48.sp, + ), + bodySmall = TextStyle( + fontSize = 14.sp, + fontFamily = Poppins, + fontWeight = FontWeight.Normal, + lineHeight = 21.sp, + ), + bodyMedium = TextStyle( + fontSize = 16.sp, + fontFamily = Poppins, + fontWeight = FontWeight.Normal, + lineHeight = 24.sp, + ), + labelSmall = TextStyle( + fontSize = 13.sp, + fontFamily = Poppins, + fontWeight = FontWeight.Normal, + lineHeight = 19.sp, + ) +) \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/utils/Constants.kt b/app/src/main/java/com/example/livingai/utils/Constants.kt new file mode 100644 index 0000000..c4327de --- /dev/null +++ b/app/src/main/java/com/example/livingai/utils/Constants.kt @@ -0,0 +1,17 @@ +package com.example.livingai.utils + +object Constants { + const val USER_SETTINGS = "USER_SETTINGS" + const val APP_ENTRY = "APP_ENTRY" + const val ANIMAL_DATA_FILENAME = "animal_data.csv" + + val silhouetteList = listOf( + "front", + "back", + "left", + "right", + "leftangle", + "rightangle", + "angleview" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/utils/IdGenerator.kt b/app/src/main/java/com/example/livingai/utils/IdGenerator.kt new file mode 100644 index 0000000..410eaf2 --- /dev/null +++ b/app/src/main/java/com/example/livingai/utils/IdGenerator.kt @@ -0,0 +1,9 @@ +package com.example.livingai.utils + +import java.util.UUID + +object IdGenerator { + fun generateAnimalId(): String { + return UUID.randomUUID().toString() + } +} diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/splash.png b/app/src/main/res/drawable/splash.png new file mode 100644 index 0000000..4dfb164 Binary files /dev/null and b/app/src/main/res/drawable/splash.png differ diff --git a/app/src/main/res/drawable/splash80.png b/app/src/main/res/drawable/splash80.png new file mode 100644 index 0000000..8296eb1 Binary files /dev/null and b/app/src/main/res/drawable/splash80.png differ diff --git a/app/src/main/res/drawable/splash_icon.xml b/app/src/main/res/drawable/splash_icon.xml new file mode 100644 index 0000000..e5c066c --- /dev/null +++ b/app/src/main/res/drawable/splash_icon.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/src/main/res/font/poppins_bold.ttf b/app/src/main/res/font/poppins_bold.ttf new file mode 100644 index 0000000..00559ee Binary files /dev/null and b/app/src/main/res/font/poppins_bold.ttf differ diff --git a/app/src/main/res/font/poppins_regular.ttf b/app/src/main/res/font/poppins_regular.ttf new file mode 100644 index 0000000..9f0c71b Binary files /dev/null and b/app/src/main/res/font/poppins_regular.ttf differ diff --git a/app/src/main/res/font/poppins_semibold.ttf b/app/src/main/res/font/poppins_semibold.ttf new file mode 100644 index 0000000..74c726e Binary files /dev/null and b/app/src/main/res/font/poppins_semibold.ttf differ diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml new file mode 100644 index 0000000..53000c4 --- /dev/null +++ b/app/src/main/res/values-hi/strings.xml @@ -0,0 +1,72 @@ + + लिविंगएआई + भाषा चुनें + अपनी पसंदीदा भाषा चुनें। हमारे पास अंग्रेजी और हिंदी है। + पशु बेचें + लिविंगएआई खरीदारों और विक्रेताओं को पूरे भारत में जोड़ने के लिए एक मंच प्रदान करता है। + पशु खरीदें + लिविंगएआई एक क्यूरेटेड एआई संचालित रेटिंग प्रदान करता है, ताकि आप अपनी आवश्यकतानुसार चुन सकें। + अन्य सुविधाएं + लिविंगएआई पशुपालन से संबंधित आपूर्ति और मदद प्राप्त करने के लिए वन-स्टॉप है। + अंग्रेज़ी + हिंदी + पीछे + अगला + शुरू करें + गाय का विवरण जोड़ें + प्रजाति + नस्ल + उम्र (वर्ष) + दूध की पैदावार (लीटर) + ब्याने की संख्या + प्रजनन स्थिति + गर्भवती + ब्याई हुई + कोई नहीं + विवरण + प्रोफ़ाइल सहेजें + रद्द करें + छवि + मीडिया अपलोड करें + लिंग + वजन (किग्रा) + संपादित करें + रेट करें + हटाएं + पशु प्रोफाइल + प्रोफ़ाइल जोड़ें + रेटिंग्स + सेटिंग्स + भाषा + ऑटो कैप्चर + + + समग्र रेटिंग + स्वास्थ्य रेटिंग + नस्ल रेटिंग + कद + छाती की चौड़ाई + शरीर की गहराई + कोणीयता + रम्प कोण + रम्प चौड़ाई + पिछले पैर का सेट + पिछले पैर का पिछला दृश्य + पैर का कोण + अगला अडर अटैचमेंट + पिछले अडर की ऊंचाई + केंद्रीय स्नायुबंधन + अडर गहराई + सामने के थन की स्थिति + थन की लंबाई + पीछे के थन की स्थिति + लोकोमोशन + शरीर की स्थिति स्कोर + हॉक विकास + हड्डी की संरचना + पिछले अडर की चौड़ाई + थन की मोटाई + मांसपेशियां + टिप्पणियाँ + रेटिंग सहेजें + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..aaf49fc --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,25 @@ + + + + English + Hindi + + + en + hi + + + Cow + Buffalo + Goat + Sheep + + + Gir + Sahiwal + Red Sindhi + Tharparkar + Holstein Friesian + Jersey + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..c5a3541 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,21 @@ + + + #59AC77 + #3A6F43 + #FDAAAA + #8E3F7F + #6F3A66 + #3E6F48 + #000000 + #000000 + #FFFFFFFF + #AC598E + #6F3A66 + #4C8B5C + #000000 + #000000 + #FFFFFFFF + #FFFFFFFF + #F9F9F9 + #3B3A3A + \ No newline at end of file diff --git a/app/src/main/res/values/splash.xml b/app/src/main/res/values/splash.xml new file mode 100644 index 0000000..2c5991e --- /dev/null +++ b/app/src/main/res/values/splash.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..9cf5d5d --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,85 @@ + + LivingAI + Select Language + Select Your Preferred Language. We have English or Hindi. + Sell Animals + LivingAI provides one platform to connect buyers and sellers across India. + Buy Animals + LivingAI provides a curated AI driven rating, so you can choose as you need. + Other Amenities + LivingAI is a one stop to procure supplies and help related to Animal Husbandry. + English + Hindi + Back + Next + Get Started + Add Cow Details + Species + Breed + Age (Years) + Milk Yield (Liters) + Calving Number + Reproductive Status + Pregnant + Calved + None + Description + Save Profile + Cancel + Image + Upload Media + Sex + Weight (kg) + Edit + Rate + Delete + Animal Profiles + Add Profile + Ratings + Settings + Language + Auto Capture + Add Photo + Add Video + Camera + Video Recorder + + + Overall Rating + Health Rating + Breed Rating + Stature + Chest Width + Body Depth + Angularity + Rump Angle + Rump Width + Rear Leg Set + Rear Leg Rear View + Foot Angle + Fore Udder Attachment + Rear Udder Height + Central Ligament + Udder Depth + Front Teat Position + Teat Length + Rear Teat Position + Locomotion + Body Condition Score + Hock Development + Bone Structure + Rear Udder Width + Teat Thickness + Muscularity + Comments + Save Ratings + + + Left View + Right View + Front View + Back View + Left Angle View + Right Angle View + Angle View + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..3c6d448 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +