diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 44c7368..a7b19ba 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,31 +1,34 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
- alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.compose.compiler)
id("com.google.gms.google-services")
}
android {
namespace = "com.samuel.inventorymanager"
- // Consider checking for the latest stable API levels.
- // compileSdk = 36 is fine, but adjust as necessary for future compatibility.
- compileSdk = 36
+ compileSdk = 34
defaultConfig {
applicationId = "com.samuel.inventorymanager"
minSdk = 24
- targetSdk = 36
+ targetSdk = 34
versionCode = 1
versionName = "1.0"
-
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
+
packaging {
resources {
- excludes += "META-INF/DEPENDENCIES"
+ excludes += "/META-INF/INDEX.LIST"
+ excludes += "/META-INF/*.md"
+ excludes += "/META-INF/DEPENDENCIES"
+ excludes += "/META-INF/LICENSE"
+ excludes += "/META-INF/LICENSE.txt"
+ excludes += "/META-INF/NOTICE"
+ excludes += "/META-INF/NOTICE.txt"
}
}
- // ==
buildTypes {
release {
isMinifyEnabled = false
@@ -47,55 +50,77 @@ android {
}
}
-// Ensure all dependencies are inside this block with curly braces
dependencies {
- // --- Google Services ---
- // For Google Sign-In and other Google Play services
- implementation("com.google.android.gms:play-services-auth:21.0.0")
- implementation("com.google.firebase:firebase-auth:22.3.1")
- implementation("com.google.firebase:firebase-core:21.1.1")
- implementation("com.google.firebase:firebase-analytics")
- implementation(platform("com.google.firebase:firebase-bom:34.5.0"))
- implementation("com.google.firebase:firebase-auth")
+ // --- Platforms (BOMs) ---
+ implementation(platform("androidx.compose:compose-bom:2024.05.00"))
+ implementation(platform("com.google.firebase:firebase-bom:33.1.0"))
-// Use one, consistent version
+ // --- AndroidX & Jetpack Compose ---
+ implementation("androidx.core:core-ktx:1.13.1")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
+ implementation("androidx.activity:activity-compose:1.9.0")
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.ui:ui-graphics")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.material:material-icons-extended")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.3")
+ implementation("androidx.navigation:navigation-compose:2.7.7")
- // For Google One Tap Sign-In
- implementation("androidx.credentials:credentials:1.2.0")
- implementation("androidx.credentials:credentials-play-services-auth:1.2.0")
+ // --- Coroutines ---
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
+
+ // --- Accompanist ---
+ implementation("com.google.accompanist:accompanist-pager:0.34.0")
+ implementation("com.google.accompanist:accompanist-pager-indicators:0.34.0")
+
+ // --- Firebase, Auth, & Credentials ---
+ implementation("com.google.firebase:firebase-analytics")
+ implementation("com.google.firebase:firebase-auth")
+ implementation("com.google.firebase:firebase-database")
+ implementation("com.google.android.gms:play-services-auth:21.2.0")
+ implementation("androidx.credentials:credentials:1.3.0-alpha01")
+ implementation("androidx.credentials:credentials-play-services-auth:1.3.0-alpha01")
implementation("com.google.android.libraries.identity.googleid:googleid:1.1.0")
+ implementation("com.google.http-client:google-http-client-android:1.43.3")
+ implementation("com.google.zxing:core:3.5.3")
- // --- Google Drive API ---
- // Required for DriveScopes and interacting with the Drive API
- implementation("com.google.api-client:google-api-client-android:2.2.0")
- implementation("com.google.apis:google-api-services-drive:v3-rev20220815-2.0.0")
- // --- AndroidX & Jetpack Compose ---
- implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.lifecycle.runtime.ktx)
- implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.4")
- implementation(libs.androidx.activity.compose)
- implementation(platform(libs.androidx.compose.bom))
- implementation("androidx.activity:activity-compose:1.9.3")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
- implementation(libs.androidx.compose.ui)
- implementation(libs.androidx.compose.ui.graphics)
- implementation(libs.androidx.compose.ui.tooling.preview)
- implementation(libs.androidx.compose.material3)
- implementation("androidx.compose.material:material-icons-extended-android:1.7.8")
+ // --- **CORRECTED** GOOGLE DRIVE API DEPENDENCIES ---
+ implementation("com.google.http-client:google-http-client-gson:1.44.1") {
+ exclude(group = "org.apache.httpcomponents")
+ }
+ implementation("com.google.api-client:google-api-client:2.4.0") {
+ exclude(group = "org.apache.httpcomponents")
+ }
+ implementation("com.google.api-client:google-api-client-android:2.4.0") {
+ exclude(group = "org.apache.httpcomponents")
+ }
+ // THIS VERSION EXISTS. MY OLD ONE DID NOT.
+ implementation("com.google.apis:google-api-services-drive:v3-rev20220815-2.0.0") {
+ exclude(group = "org.apache.httpcomponents")
+ }
- // --- Utility ---
- // Gson for JSON processing
- implementation("com.google.code.gson:gson:2.13.2") // Kept one instance
- // Coil for image loading
- implementation("io.coil-kt:coil-compose:2.7.0")
+ // --- Image & ML Kit (WITH CORRECT VERSIONS) ---
+ implementation("com.vanniktech:android-image-cropper:4.5.0")
+ implementation("io.coil-kt:coil-compose:2.6.0")
+ implementation("com.google.android.gms:play-services-mlkit-text-recognition:19.0.0")
+ implementation("com.google.mlkit:text-recognition:16.0.0")
+ // THIS VERSION EXISTS. MY OLD ONE DID NOT.
+ implementation("com.google.mlkit:image-labeling:17.0.8")
+ implementation("com.google.mlkit:object-detection:17.0.1")
+
+ // --- JSON Parsing (WITH CORRECT VERSION) ---
+ implementation("com.google.code.gson:gson:2.10.1")
// --- Testing ---
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
- androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(platform("androidx.compose:compose-bom:2024.05.00"))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
+
+ // --- Debug ---
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2b23537..63ad4ed 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,23 +8,27 @@
android:required="false" />
+
+
+
+
+ tools:ignore="AllFilesAccessPolicy,ScopedStorage" />
+
+
-
+
Unit,
+ onCancel: () -> Unit
+) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Edit Image") },
+ navigationIcon = {
+ IconButton(onClick = onCancel) {
+ Icon(Icons.Default.Close, null)
+ }
+ },
+ actions = {
+ TextButton(onClick = onSave) {
+ Text("Save")
+ }
+ }
+ )
+ }
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding)
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Image(
+ painter = rememberAsyncImagePainter(uri),
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(16.dp)),
+ contentScale = ContentScale.Fit
+ )
+
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ AssistChip(onClick = { }, label = { Text("Crop" ) }, leadingIcon = { Icon(Icons.Default.Crop, null) })
+ AssistChip(onClick = { }, label = { Text("Rotate") }, leadingIcon = { Icon(Icons.Default.RotateRight, null) })
+ AssistChip(onClick = { }, label = { Text("Brightness") }, leadingIcon = { Icon(Icons.Default.Brightness4, null) })
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/MainActivity.kt b/app/src/main/java/com/samuel/inventorymanager/MainActivity.kt
index 414c73e..0a34fe2 100644
--- a/app/src/main/java/com/samuel/inventorymanager/MainActivity.kt
+++ b/app/src/main/java/com/samuel/inventorymanager/MainActivity.kt
@@ -1,13 +1,30 @@
-package com.samuel.inventorymanager // Ensure this matches your package name
+@file:Suppress("DEPRECATION")
+
+package com.samuel.inventorymanager
import android.os.Bundle
+import android.util.Log
import androidx.activity.ComponentActivity
+import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
+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.platform.LocalContext
+import com.google.android.gms.auth.api.signin.GoogleSignIn
+import com.google.android.gms.auth.api.signin.GoogleSignInAccount
+import com.google.android.gms.auth.api.signin.GoogleSignInOptions
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.tasks.Task
import com.samuel.inventorymanager.screens.MainAppScreen
+import com.samuel.inventorymanager.screens.OnboardingScreen
import com.samuel.inventorymanager.ui.theme.InventoryManagerTheme
class MainActivity : ComponentActivity() {
@@ -15,14 +32,56 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
InventoryManagerTheme {
- // A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
- MainAppScreen()
+ AppEntry()
}
}
}
}
+}
+
+@Composable
+fun AppEntry() {
+ val context = LocalContext.current
+ var googleAccount by remember {
+ mutableStateOf(GoogleSignIn.getLastSignedInAccount(context))
+ }
+
+ val gso = remember {
+ GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
+ .requestEmail()
+ .build()
+ }
+
+ val googleSignInClient = remember {
+ GoogleSignIn.getClient(context, gso)
+ }
+
+ val signInLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ val task: Task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
+ try {
+ val account = task.getResult(ApiException::class.java)!!
+ Log.d("SIGN_IN_SUCCESS", "Signed in as: ${account.email}")
+ googleAccount = account
+ } catch (e: ApiException) {
+ Log.w("SIGN_IN_ERROR", "signInResult:failed code=" + e.statusCode)
+ }
+ }
+
+ if (googleAccount == null) {
+ OnboardingScreen(
+ onGetStarted = {},
+ onSignInWithGoogle = {
+ val signInIntent = googleSignInClient.signInIntent
+ signInLauncher.launch(signInIntent)
+ }
+ )
+ } else {
+ MainAppScreen()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/data/AppSettings.kt b/app/src/main/java/com/samuel/inventorymanager/data/AppSettings.kt
index 481522a..0fe24b8 100644
--- a/app/src/main/java/com/samuel/inventorymanager/data/AppSettings.kt
+++ b/app/src/main/java/com/samuel/inventorymanager/data/AppSettings.kt
@@ -2,14 +2,16 @@ package com.samuel.inventorymanager.data
// Theme and appearance settings
enum class AppTheme {
- LIGHT, DARK, SYSTEM, CUSTOM
+ LIGHT, DARK, SYSTEM,
+ DRACULA, VAMPIRE, OCEAN, FOREST, SUNSET, CYBERPUNK, NEON,
+ CUSTOM
}
enum class FontSize(val scale: Float) {
SMALL(0.85f),
MEDIUM(1.0f),
- LARGE(1.15f),
- EXTRA_LARGE(1.3f)
+ LARGE(1.3f),
+ EXTRA_LARGE(1.5f)
}
data class CustomTheme(
@@ -17,38 +19,50 @@ data class CustomTheme(
val backgroundColor: Long = 0xFFFFFFFF,
val surfaceColor: Long = 0xFFFFFFFF,
val onPrimaryColor: Long = 0xFFFFFFFF,
- val fontSizeScale: Float = 1.0f // NEW: Font scaling for custom theme
+ val fontSizeScale: Float = 1.0f
)
// OCR Provider enum
enum class OCRProvider {
- ROBOFLOW, OCR_SPACE, GOOGLE_VISION
+ TESSERACT_JS,
+ ROBOFLOW,
+ OCR_SPACE,
+ OPTIIC,
+ GOOGLE_VISION
}
// AI Provider enum
enum class AIProvider {
- GOOGLE_GEMINI, OPENAI
+ GOOGLE_GEMINI,
+ OPENAI,
+ SMART_OFFLINE
}
-// OCR settings with priority
+// OCR settings with priority - FIXED to include ALL providers
data class OCRSettings(
val roboflowApiKey: String = "",
val ocrSpaceApiKey: String = "",
val googleVisionApiKey: String = "",
+ val optiicApiKey: String = "",
val providerPriority: List = listOf(
+ OCRProvider.TESSERACT_JS,
OCRProvider.ROBOFLOW,
OCRProvider.OCR_SPACE,
+ OCRProvider.OPTIIC,
OCRProvider.GOOGLE_VISION
)
)
-// AI settings with priority
+// AI settings with priority - FIXED to include ALL providers
data class AISettings(
+
+ val anthropicApiKey: String = "", // Add this field
val googleGeminiApiKey: String = "",
val openAIApiKey: String = "",
val providerPriority: List = listOf(
AIProvider.GOOGLE_GEMINI,
- AIProvider.OPENAI
+ AIProvider.OPENAI,
+ AIProvider.SMART_OFFLINE
)
)
@@ -57,14 +71,14 @@ data class GoogleSettings(
val signedIn: Boolean = false,
val userEmail: String = "",
val autoBackupToDrive: Boolean = false,
- val lastBackupTime: Long = 0 // NEW: Track last backup timestamp
+ val lastBackupTime: Long = 0
)
// Auto features settings
data class AutoFeatures(
val autoGoogleBackup: Boolean = false,
val autoLocalSave: Boolean = true,
- val lastLocalSaveTime: Long = 0 // NEW: Track last local save timestamp
+ val lastLocalSaveTime: Long = 0
)
// COMPLETE AppSettings with ALL properties
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/AIProcessingScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/AIProcessingScreen.kt
new file mode 100644
index 0000000..1e7f5fd
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/AIProcessingScreen.kt
@@ -0,0 +1,306 @@
+package com.samuel.inventorymanager.screens
+
+import android.graphics.Bitmap
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.animation.togetherWith
+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.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.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Psychology
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+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.draw.clip
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.samuel.inventorymanager.services.AIService
+import com.samuel.inventorymanager.services.OCRService
+import kotlinx.coroutines.delay
+
+@Composable
+fun AIProcessingScreen(
+ bitmap: Bitmap,
+ onComplete: (AIAnalysisResult) -> Unit,
+ onError: (String) -> Unit
+) {
+ val context = LocalContext.current
+
+ var currentStep by remember { mutableStateOf(0) }
+ var progress by remember { mutableFloatStateOf(0f) }
+
+ val steps = listOf(
+ "🔍 Analyzing image...",
+ "📝 Extracting text...",
+ "🤖 Understanding content...",
+ "✨ Generating details..."
+ )
+
+ // Animate progress
+ LaunchedEffect(currentStep) {
+ while (currentStep < steps.size) {
+ delay(800)
+ progress = (currentStep + 1) / steps.size.toFloat()
+ if (currentStep < steps.size - 1) {
+ currentStep++
+ } else {
+ break
+ }
+ }
+ }
+
+ // Perform actual AI processing
+ LaunchedEffect(Unit) {
+ try {
+ // Step 1: OCR
+ currentStep = 0
+ delay(500)
+ val ocrService = OCRService(context)
+ val tempUri = saveBitmapToTempUri(context, bitmap)
+ val ocrResult = ocrService.performOCR(tempUri)
+
+ // Step 2: AI Analysis
+ currentStep = 1
+ delay(500)
+ val aiService = AIService(context)
+
+ currentStep = 2
+ delay(500)
+ val aiResult = aiService.analyzeItemFromBitmap(bitmap)
+
+ // Step 3: Combine results
+ currentStep = 3
+ delay(500)
+
+ val finalResult = AIAnalysisResult(
+ itemName = aiResult.itemName ?: ocrResult.text.lines().firstOrNull(),
+ modelNumber = aiResult.modelNumber,
+ description = aiResult.description ?: ocrResult.text,
+ estimatedPrice = aiResult.estimatedPrice,
+ dimensions = aiResult.dimensions,
+ rawText = ocrResult.text
+ )
+
+ delay(300)
+ onComplete(finalResult)
+
+ } catch (e: Exception) {
+ onError(e.message ?: "AI processing failed")
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ Brush.verticalGradient(
+ colors = listOf(
+ Color(0xFF0F172A),
+ Color(0xFF1E293B),
+ Color(0xFF312E81)
+ )
+ )
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(32.dp),
+ modifier = Modifier.padding(32.dp)
+ ) {
+ // Animated AI Icon
+ AILoadingAnimation()
+
+ // Progress Text
+ Text(
+ "AI Magic in Progress",
+ color = Color.White,
+ fontSize = 28.sp,
+ fontWeight = FontWeight.Bold
+ )
+
+ // Current Step
+ AnimatedContent(
+ targetState = steps[currentStep],
+ transitionSpec = {
+ fadeIn() + slideInVertically { it } togetherWith
+ fadeOut() + slideOutVertically { -it }
+ },
+ label = "step_animation"
+ ) { step ->
+ Text(
+ step,
+ color = Color.White.copy(alpha = 0.8f),
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Medium,
+ textAlign = TextAlign.Center
+ )
+ }
+
+ // Progress Bar
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ LinearProgressIndicator(
+ progress = { progress },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(8.dp)
+ .clip(RoundedCornerShape(4.dp)),
+ color = Color(0xFF8B5CF6),
+ trackColor = Color.White.copy(alpha = 0.2f)
+ )
+ Text(
+ "${(progress * 100).toInt()}%",
+ color = Color.White.copy(alpha = 0.6f),
+ fontSize = 14.sp,
+ modifier = Modifier.align(Alignment.End)
+ )
+ }
+
+ // Step Indicators
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.padding(top = 16.dp)
+ ) {
+ steps.forEachIndexed { index, _ ->
+ StepIndicator(
+ isComplete = index <= currentStep,
+ isActive = index == currentStep
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun AILoadingAnimation() {
+ val infiniteTransition = rememberInfiniteTransition(label = "ai_loading")
+
+ val rotation by infiniteTransition.animateFloat(
+ initialValue = 0f,
+ targetValue = 360f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(3000, easing = LinearEasing),
+ repeatMode = RepeatMode.Restart
+ ),
+ label = "rotation"
+ )
+
+ val scale by infiniteTransition.animateFloat(
+ initialValue = 0.9f,
+ targetValue = 1.1f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1000, easing = FastOutSlowInEasing),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "scale"
+ )
+
+ Box(
+ modifier = Modifier.size(120.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ // Outer rotating circle
+ Surface(
+ shape = CircleShape,
+ color = Color(0xFF8B5CF6).copy(alpha = 0.2f),
+ modifier = Modifier
+ .size(120.dp)
+ .rotate(rotation)
+ ) {}
+
+ // Inner icon
+ Surface(
+ shape = CircleShape,
+ color = Color(0xFF8B5CF6),
+ modifier = Modifier
+ .size(80.dp)
+ .scale(scale)
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ Icon(
+ Icons.Default.Psychology,
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(48.dp)
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun StepIndicator(isComplete: Boolean, isActive: Boolean) {
+ val backgroundColor by animateColorAsState(
+ targetValue = when {
+ isComplete -> Color(0xFF10B981)
+ isActive -> Color(0xFF8B5CF6)
+ else -> Color.White.copy(alpha = 0.2f)
+ },
+ label = "step_bg"
+ )
+
+ val size by animateDpAsState(
+ targetValue = if (isActive) 12.dp else 8.dp,
+ label = "step_size"
+ )
+
+ Surface(
+ shape = CircleShape,
+ color = backgroundColor,
+ modifier = Modifier.size(size)
+ ) {}
+}
+
+// Helper function
+private fun saveBitmapToTempUri(context: android.content.Context, bitmap: Bitmap): android.net.Uri {
+ val file = java.io.File(context.cacheDir, "temp_ai_${System.currentTimeMillis()}.jpg")
+ file.outputStream().use { out ->
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
+ }
+ return android.net.Uri.fromFile(file)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/AIResultsScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/AIResultsScreen.kt
new file mode 100644
index 0000000..8319e13
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/AIResultsScreen.kt
@@ -0,0 +1,415 @@
+package com.samuel.inventorymanager.screens
+
+import android.graphics.Bitmap
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.foundation.Image
+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.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.AspectRatio
+import androidx.compose.material.icons.filled.AttachMoney
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.CheckCircle
+import androidx.compose.material.icons.filled.Description
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Label
+import androidx.compose.material.icons.filled.QrCode
+import androidx.compose.material.icons.filled.TextFields
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Surface
+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.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun AIResultsScreen(
+ bitmap: Bitmap,
+ result: AIAnalysisResult,
+ onBackToEdit: () -> Unit,
+ onSaveAndContinue: (AIAnalysisResult) -> Unit
+) {
+ var showSuccessAnimation by remember { mutableStateOf(true) }
+
+ LaunchedEffect(Unit) {
+ kotlinx.coroutines.delay(2000)
+ showSuccessAnimation = false
+ }
+
+ Box(modifier = Modifier.fillMaxSize()) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ Brush.verticalGradient(
+ colors = listOf(
+ Color(0xFF0F172A),
+ Color(0xFF1E293B)
+ )
+ )
+ )
+ ) {
+ // Top Bar
+ ResultsTopBar(
+ onBackToEdit = onBackToEdit,
+ onSave = { onSaveAndContinue(result) }
+ )
+
+ // Content
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(20.dp)
+ ) {
+ // Image Preview Card
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp),
+ shape = RoundedCornerShape(20.dp),
+ elevation = CardDefaults.cardElevation(8.dp)
+ ) {
+ Image(
+ bitmap = bitmap.asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
+ }
+
+ // Success Badge
+ AnimatedVisibility(
+ visible = showSuccessAnimation,
+ enter = fadeIn() + scaleIn(),
+ exit = fadeOut() + scaleOut()
+ ) {
+ SuccessBadge()
+ }
+
+ // Results Card
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(24.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ ),
+ elevation = CardDefaults.cardElevation(8.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(24.dp),
+ verticalArrangement = Arrangement.spacedBy(20.dp)
+ ) {
+ Text(
+ "✨ AI Extracted Details",
+ color = Color.White,
+ fontSize = 22.sp,
+ fontWeight = FontWeight.Bold
+ )
+
+ HorizontalDivider(color = Color.White.copy(alpha = 0.1f))
+
+ // Item Name
+ result.itemName?.let { name ->
+ ResultField(
+ icon = Icons.Default.Label,
+ label = "Item Name",
+ value = name,
+ color = Color(0xFF8B5CF6)
+ )
+ }
+
+ // Model Number
+ result.modelNumber?.let { model ->
+ ResultField(
+ icon = Icons.Default.QrCode,
+ label = "Model Number",
+ value = model,
+ color = Color(0xFF10B981)
+ )
+ }
+
+ // Description
+ result.description?.let { desc ->
+ ResultField(
+ icon = Icons.Default.Description,
+ label = "Description",
+ value = desc,
+ color = Color(0xFF3B82F6)
+ )
+ }
+
+ // Dimensions
+ result.dimensions?.let { dims ->
+ ResultField(
+ icon = Icons.Default.AspectRatio,
+ label = "Dimensions",
+ value = dims,
+ color = Color(0xFFEC4899)
+ )
+ }
+
+ // Estimated Price
+ result.estimatedPrice?.let { price ->
+ ResultField(
+ icon = Icons.Default.AttachMoney,
+ label = "Estimated Value",
+ value = "$${String.format("%.2f", price)}",
+ color = Color(0xFF10B981)
+ )
+ }
+ }
+ }
+
+ // Detected Text Card (if available)
+ result.rawText?.takeIf { it.isNotBlank() }?.let { text ->
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(20.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B).copy(alpha = 0.6f)
+ )
+ ) {
+ Column(
+ modifier = Modifier.padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(
+ Icons.Default.TextFields,
+ contentDescription = null,
+ tint = Color(0xFF8B5CF6),
+ modifier = Modifier.size(24.dp)
+ )
+ Text(
+ "Detected Text",
+ color = Color.White,
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ Text(
+ text,
+ color = Color.White.copy(alpha = 0.8f),
+ fontSize = 14.sp,
+ lineHeight = 20.sp
+ )
+ }
+ }
+ }
+
+ // Action Buttons
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ OutlinedButton(
+ onClick = onBackToEdit,
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(16.dp),
+ border = androidx.compose.foundation.BorderStroke(
+ 2.dp,
+ Color.White.copy(alpha = 0.3f)
+ ),
+ colors = ButtonDefaults.outlinedButtonColors(
+ contentColor = Color.White
+ )
+ ) {
+ Icon(Icons.Default.Edit, null)
+ Spacer(Modifier.width(8.dp))
+ Text("Edit Photo", fontWeight = FontWeight.Bold)
+ }
+
+ Button(
+ onClick = { onSaveAndContinue(result) },
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(16.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color(0xFF10B981)
+ )
+ ) {
+ Icon(Icons.Default.Check, null)
+ Spacer(Modifier.width(8.dp))
+ Text("Save Item", fontWeight = FontWeight.Bold)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ResultsTopBar(onBackToEdit: () -> Unit, onSave: () -> Unit) {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ color = Color(0xFF1E293B).copy(alpha = 0.95f),
+ shadowElevation = 8.dp
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ IconButton(
+ onClick = onBackToEdit,
+ modifier = Modifier
+ .size(48.dp)
+ .background(Color.White.copy(alpha = 0.1f), CircleShape)
+ ) {
+ Icon(
+ Icons.Default.ArrowBack,
+ contentDescription = "Back to edit",
+ tint = Color.White,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+
+ Text(
+ "🎯 AI Results",
+ color = Color.White,
+ fontSize = 20.sp,
+ fontWeight = FontWeight.Bold
+ )
+
+ IconButton(
+ onClick = onSave,
+ modifier = Modifier
+ .size(48.dp)
+ .background(Color(0xFF10B981), CircleShape)
+ ) {
+ Icon(
+ Icons.Default.Check,
+ contentDescription = "Save",
+ tint = Color.White,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun SuccessBadge() {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(20.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF10B981).copy(alpha = 0.2f)
+ ),
+ border = androidx.compose.foundation.BorderStroke(2.dp, Color(0xFF10B981))
+ ) {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Surface(
+ shape = CircleShape,
+ color = Color(0xFF10B981),
+ modifier = Modifier.size(48.dp)
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ Icon(
+ Icons.Default.CheckCircle,
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(28.dp)
+ )
+ }
+ }
+ Text(
+ "AI analysis complete! Review the details below.",
+ color = Color.White,
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium
+ )
+ }
+ }
+}
+
+@Composable
+fun ResultField(
+ icon: androidx.compose.ui.graphics.vector.ImageVector,
+ label: String,
+ value: String,
+ color: Color
+) {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(
+ icon,
+ contentDescription = null,
+ tint = color,
+ modifier = Modifier.size(20.dp)
+ )
+ Text(
+ label,
+ color = Color.White.copy(alpha = 0.6f),
+ fontSize = 13.sp,
+ fontWeight = FontWeight.Medium
+ )
+ }
+ Surface(
+ shape = RoundedCornerShape(12.dp),
+ color = color.copy(alpha = 0.1f),
+ border = androidx.compose.foundation.BorderStroke(1.dp, color.copy(alpha = 0.3f))
+ ) {
+ Text(
+ value,
+ color = Color.White,
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Medium,
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/CreateItemScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/CreateItemScreen.kt
index 5c8f960..815cdc8 100644
--- a/app/src/main/java/com/samuel/inventorymanager/screens/CreateItemScreen.kt
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/CreateItemScreen.kt
@@ -3,10 +3,17 @@ package com.samuel.inventorymanager.screens
import android.Manifest
import android.content.Context
import android.net.Uri
+import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.EaseInOut
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
@@ -18,36 +25,47 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
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.layout.width
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Notes
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.ConfirmationNumber
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.DocumentScanner
import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.Psychology
+import androidx.compose.material.icons.filled.SaveAs
+import androidx.compose.material.icons.filled.Update
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Snackbar
@@ -56,7 +74,6 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
-import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -68,29 +85,36 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
import androidx.core.content.FileProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.rememberAsyncImagePainter
import com.samuel.inventorymanager.data.AppSettings
+import com.samuel.inventorymanager.services.AIService
+import com.samuel.inventorymanager.services.OCRService
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
-import java.util.UUID
-// Import AppSettings from data package
-//import com.samuel.inventorymanager.data.SettingsData
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreateItemScreen(
+ items: List- ,
garages: List,
onSaveItem: (Item) -> Unit,
+ onUpdateItem: (Item) -> Unit,
+ onDeleteItem: (Item) -> Unit,
viewModel: CreateItemViewModel = viewModel(),
appSettings: AppSettings,
onSettingsChange: (AppSettings) -> Unit
@@ -99,11 +123,27 @@ fun CreateItemScreen(
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
+ // Image Processing States
+ var showImageProcessing by remember { mutableStateOf(false) }
+ var capturedImageUri by remember { mutableStateOf(null) }
+ var tempCameraUri by remember { mutableStateOf(null) }
+
+ // Dialog States
var showUnsavedWarning by remember { mutableStateOf(false) }
+ var showDuplicateDialog by remember { mutableStateOf(false) }
+ var showDeleteConfirmDialog by remember { mutableStateOf(false) }
+ var duplicateItem by remember { mutableStateOf
- (null) }
var showCameraPreferenceBanner by remember { mutableStateOf(false) }
+
+ // Processing States
+ var isProcessingOCR by remember { mutableStateOf(false) }
+ var isProcessingAI by remember { mutableStateOf(false) }
+
+ // Auto-save
var autoSaveEnabled by remember { mutableStateOf(true) }
var lastAutoSaveTime by remember { mutableLongStateOf(0L) }
+ // Dynamic options based on selection
val garageOptions = remember(garages) { garages.map { it.name } }
val cabinetOptions = remember(viewModel.selectedGarageName, garages) {
garages.find { it.name == viewModel.selectedGarageName }?.cabinets?.map { it.name } ?: emptyList()
@@ -120,51 +160,111 @@ fun CreateItemScreen(
?.boxes?.map { it.name } ?: emptyList()
}
- var tempCameraUri by remember { mutableStateOf(null) }
+ // ==================== FUNCTIONS: Helpers ====================
+ fun createImageUri(context: Context): Uri {
+ val directory = File(context.cacheDir, "images")
+ if (!directory.exists()) directory.mkdirs()
+ val file = File(directory, "IMG_${System.currentTimeMillis()}.jpg")
+ return FileProvider.getUriForFile(context, context.packageName + ".fileprovider", file)
+ }
- val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { success ->
- if (success) {
- tempCameraUri?.let {
- viewModel.imageUris.add(it)
- viewModel.checkForChanges()
- }
+ // ==================== LAUNCHERS ====================
+
+ val cameraLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.TakePicture()
+ ) { success ->
+ if (success && tempCameraUri != null) {
+ capturedImageUri = tempCameraUri
+ showImageProcessing = true
}
}
- val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
+ val permissionLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted ->
if (isGranted) {
val uri = createImageUri(context)
tempCameraUri = uri
+ capturedImageUri = uri
cameraLauncher.launch(uri)
+ } else {
+ scope.launch { snackbarHostState.showSnackbar("📷 Camera permission required") }
}
}
- val galleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
- viewModel.imageUris.addAll(uris)
- viewModel.checkForChanges()
+ val galleryLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.GetMultipleContents()
+ ) { uris ->
+ if (uris.isNotEmpty()) {
+ viewModel.imageUris.addAll(uris)
+ viewModel.checkForChanges()
+ scope.launch { snackbarHostState.showSnackbar("✅ ${uris.size} image(s) added") }
+ }
}
+ // ==================== FUNCTIONS ====================
+
fun launchCameraWithPermissionCheck() {
permissionLauncher.launch(Manifest.permission.CAMERA)
}
+ fun findDuplicateItem(): Item? {
+ val itemName = viewModel.itemName.trim()
+ val modelNumber = viewModel.modelNumber.trim()
+
+ return items.find { existingItem ->
+ val nameMatch = existingItem.name.equals(itemName, ignoreCase = true)
+ val modelMatch = if (modelNumber.isNotBlank() && existingItem.modelNumber != null) {
+ existingItem.modelNumber.equals(modelNumber, ignoreCase = true)
+ } else false
+
+ nameMatch || modelMatch
+ }
+ }
+
fun saveItem(showNotification: Boolean = true) {
if (viewModel.itemName.isBlank()) {
scope.launch { snackbarHostState.showSnackbar("⚠️ Item name is required") }
return
}
+
+ if (viewModel.selectedGarageName.isBlank()) {
+ scope.launch { snackbarHostState.showSnackbar("⚠️ You must select a Garage!") }
+ return
+ }
+
+ val duplicate = findDuplicateItem()
+ if (duplicate != null) {
+ duplicateItem = duplicate
+ showDuplicateDialog = true
+ return
+ }
+
onSaveItem(viewModel.getItemToSave(garages))
viewModel.markAsSaved()
if (showNotification) {
- scope.launch { snackbarHostState.showSnackbar("✓ Item saved successfully!") }
+ scope.launch { snackbarHostState.showSnackbar("✅ Item created successfully!") }
}
}
- fun handleNewItemAndCamera() {
+ fun updateExistingItem(item: Item) {
+ val updatedItem = viewModel.getItemToSave(garages).copy(id = item.id)
+ onUpdateItem(updatedItem)
+ viewModel.markAsSaved()
+ scope.launch { snackbarHostState.showSnackbar("✅ Item updated successfully!") }
+ }
+
+ fun createNewItemAnyway() {
+ onSaveItem(viewModel.getItemToSave(garages))
+ viewModel.markAsSaved()
+ scope.launch { snackbarHostState.showSnackbar("✅ New item created!") }
+ }
+
+ fun handleNewItemClick() {
if (viewModel.hasUnsavedChanges) {
showUnsavedWarning = true
} else {
- viewModel.clearFormForNewItem(garages)
+ viewModel.clearFormForNewItem()
if (appSettings.openCameraOnNewItem) {
if (!appSettings.hasShownCameraPreference) {
showCameraPreferenceBanner = true
@@ -175,6 +275,90 @@ fun CreateItemScreen(
}
}
+ fun performOCR() {
+ if (viewModel.imageUris.isEmpty()) {
+ scope.launch { snackbarHostState.showSnackbar("⚠️ Please add an image first!") }
+ return
+ }
+
+ scope.launch {
+ isProcessingOCR = true
+ try {
+ val ocrService = OCRService(context)
+ val result = ocrService.performOCR(viewModel.imageUris.first(), appSettings.ocrSettings)
+
+ val lines = result.text.lines().filter { it.isNotBlank() }
+ if (lines.isNotEmpty()) {
+ viewModel.itemName = lines.firstOrNull() ?: ""
+ if (lines.size > 1) viewModel.modelNumber = lines[1]
+ if (lines.size > 2) viewModel.description = lines.drop(2).joinToString("\n")
+ }
+
+ snackbarHostState.showSnackbar("✅ OCR Complete! (${result.provider})")
+ viewModel.checkForChanges()
+ } catch (e: Exception) {
+ snackbarHostState.showSnackbar("❌ OCR Failed: ${e.message}")
+ } finally {
+ isProcessingOCR = false
+ }
+ }
+ }
+
+ fun performAI() {
+ if (viewModel.imageUris.isEmpty() && viewModel.itemName.isBlank()) {
+ scope.launch { snackbarHostState.showSnackbar("⚠️ Please add an image or item name first!") }
+ return
+ }
+ scope.launch {
+ isProcessingAI = true
+ try {
+ val aiService = AIService(context)
+ val result: AIService.AIAnalysisResult =
+ if (viewModel.imageUris.isNotEmpty()) {
+ aiService.analyzeItemFromBitmap(
+ android.graphics.BitmapFactory.decodeStream(
+ context.contentResolver.openInputStream(viewModel.imageUris.first())
+ )!!
+ )
+ } else {
+ // Fixed: Provide all required parameters
+ AIService.AIAnalysisResult(
+ itemName = null,
+ confidence = 0.0,
+ modelNumber = null,
+ description = "Please add an image for AI analysis",
+ estimatedPrice = null,
+ condition = null,
+ sizeCategory = null,
+ dimensions = null,
+ rawText = null
+ )
+ }
+ result.itemName?.let { viewModel.itemName = it }
+ result.modelNumber?.let { viewModel.modelNumber = it }
+ result.description?.let { viewModel.description = it }
+ result.condition?.let { viewModel.condition = it }
+ result.sizeCategory?.let { viewModel.sizeCategory = it }
+ result.estimatedPrice?.let { price ->
+ viewModel.minPrice = price.toString()
+ viewModel.maxPrice = (price * 1.2).toString()
+ }
+ result.dimensions?.let { viewModel.dimensions = it }
+
+ snackbarHostState.showSnackbar("✅ AI Analysis Complete!")
+ viewModel.checkForChanges()
+ } catch (e: Exception) {
+ Log.e("performAI", "AI processing failed", e)
+ snackbarHostState.showSnackbar("AI processing failed: ${e.localizedMessage}")
+ } finally {
+ isProcessingAI = false
+ }
+ }
+ }
+
+
+ // ==================== AUTO-SAVE ====================
+
LaunchedEffect(
viewModel.itemName, viewModel.modelNumber, viewModel.description, viewModel.webLink,
viewModel.condition, viewModel.functionality, viewModel.quantity, viewModel.minPrice,
@@ -185,421 +369,839 @@ fun CreateItemScreen(
viewModel.checkForChanges()
if (autoSaveEnabled && viewModel.hasUnsavedChanges && viewModel.itemName.isNotBlank()) {
val currentTime = System.currentTimeMillis()
- if (currentTime - lastAutoSaveTime > 3000) {
- delay(3000)
+ if (currentTime - lastAutoSaveTime > 8000) {
+ delay(2000)
saveItem(false)
lastAutoSaveTime = currentTime
- scope.launch { snackbarHostState.showSnackbar("💾 Auto-saved", withDismissAction = true) }
+ scope.launch {
+ snackbarHostState.showSnackbar("💾 Auto-saved", withDismissAction = true)
+ }
}
}
}
+ // ==================== IMAGE PROCESSING DIALOG ====================
+
+ if (showImageProcessing && capturedImageUri != null) {
+ Dialog(
+ onDismissRequest = { showImageProcessing = false },
+ properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false)
+ ) {
+ ImageProcessingScreen(
+ imageUri = capturedImageUri!!,
+ onImageProcessed = { uri, aiResult ->
+ viewModel.imageUris.add(uri)
+ aiResult.itemName?.let { viewModel.itemName = it }
+ aiResult.modelNumber?.let { viewModel.modelNumber = it }
+ aiResult.description?.let {
+ viewModel.description = if (viewModel.description.isBlank()) it else "${viewModel.description}\n\n$it"
+ }
+ aiResult.condition?.let { viewModel.condition = it }
+ aiResult.sizeCategory?.let { viewModel.sizeCategory = it }
+ aiResult.estimatedPrice?.let {
+ viewModel.minPrice = it.toString()
+ viewModel.maxPrice = (it * 1.2).toString()
+ }
+ viewModel.checkForChanges()
+ showImageProcessing = false
+ scope.launch { snackbarHostState.showSnackbar("✨ AI auto-filled item details!") }
+ },
+ onCancel = { showImageProcessing = false },
+ aiService = AIService(context)
+ )
+ }
+ }
+
+ // ==================== DUPLICATE DIALOG ====================
+
+ if (showDuplicateDialog && duplicateItem != null) {
+ ModernAlertDialog(
+ onDismissRequest = { showDuplicateDialog = false },
+ icon = Icons.Default.Warning,
+ iconTint = Color(0xFFFBBF24),
+ title = "Duplicate Item Found",
+ content = {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Text(
+ "An item with this name already exists:",
+ color = Color.White.copy(alpha = 0.7f),
+ fontSize = 14.sp
+ )
+ Card(
+ colors = CardDefaults.cardColors(containerColor = Color(0xFF2A2A2A)),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ InfoRow("Name:", duplicateItem!!.name)
+ InfoRow("Qty:", "${duplicateItem!!.quantity}")
+ InfoRow("Condition:", duplicateItem!!.condition)
+ }
+ }
+ Text(
+ "Would you like to update this item or create a new one?",
+ color = Color.White.copy(alpha = 0.6f),
+ fontSize = 13.sp
+ )
+ }
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ updateExistingItem(duplicateItem!!)
+ showDuplicateDialog = false
+ duplicateItem = null
+ },
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF6366F1)),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Icon(Icons.Default.Update, null, modifier = Modifier.size(18.dp))
+ Spacer(Modifier.width(6.dp))
+ Text("Update Existing", fontWeight = FontWeight.Bold)
+ }
+ },
+ dismissButton = {
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ TextButton(onClick = { showDuplicateDialog = false; duplicateItem = null }) {
+ Text("Cancel", color = Color.White.copy(alpha = 0.7f))
+ }
+ Button(
+ onClick = {
+ createNewItemAnyway()
+ showDuplicateDialog = false
+ duplicateItem = null
+ },
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF10B981)),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Icon(Icons.Default.Add, null, modifier = Modifier.size(18.dp))
+ Spacer(Modifier.width(6.dp))
+ Text("Create New", fontWeight = FontWeight.Bold)
+ }
+ }
+ }
+ )
+ }
+
+ // ==================== UNSAVED WARNING DIALOG ====================
+
if (showUnsavedWarning) {
- AlertDialog(
+ ModernAlertDialog(
onDismissRequest = { showUnsavedWarning = false },
- icon = { Icon(Icons.Default.Warning, "Warning", tint = MaterialTheme.colorScheme.error) },
- title = { Text("Unsaved Changes", fontWeight = FontWeight.Bold) },
- text = { Text("You have unsaved changes. Do you want to save before creating a new item?") },
+ icon = Icons.Default.Warning,
+ iconTint = Color(0xFFFBBF24),
+ title = "Unsaved Changes",
+ content = {
+ Text(
+ "You have unsaved changes. Do you want to save before creating a new item?",
+ color = Color.White.copy(alpha = 0.7f),
+ fontSize = 14.sp
+ )
+ },
confirmButton = {
- Button(onClick = {
- saveItem()
- viewModel.clearFormForNewItem(garages)
- showUnsavedWarning = false
- if (appSettings.openCameraOnNewItem) {
- launchCameraWithPermissionCheck()
- }
- }) {
- Text("Save & Continue")
+ Button(
+ onClick = {
+ saveItem()
+ viewModel.clearFormForNewItem()
+ showUnsavedWarning = false
+ if (appSettings.openCameraOnNewItem) launchCameraWithPermissionCheck()
+ },
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF6366F1)),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Text("Save & Continue", fontWeight = FontWeight.Bold)
}
},
dismissButton = {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- TextButton({ showUnsavedWarning = false }) { Text("Cancel") }
+ TextButton({ showUnsavedWarning = false }) {
+ Text("Cancel", color = Color.White.copy(alpha = 0.7f))
+ }
TextButton(
onClick = {
- viewModel.clearFormForNewItem(garages)
+ viewModel.clearFormForNewItem()
showUnsavedWarning = false
- if (appSettings.openCameraOnNewItem) {
- launchCameraWithPermissionCheck()
- }
- },
- colors = ButtonDefaults.textButtonColors(
- contentColor = MaterialTheme.colorScheme.error
- )
+ if (appSettings.openCameraOnNewItem) launchCameraWithPermissionCheck()
+ }
) {
- Text("Discard")
+ Text("Discard", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
}
}
}
)
}
- Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
+ // ==================== DELETE CONFIRMATION DIALOG ====================
+
+ if (showDeleteConfirmDialog) {
+ ModernAlertDialog(
+ onDismissRequest = { showDeleteConfirmDialog = false },
+ icon = Icons.Default.Delete,
+ iconTint = Color(0xFFEF4444),
+ title = "Delete This Item?",
+ content = {
+ Text(
+ "This will permanently delete this item from your inventory. This action cannot be undone.",
+ color = Color.White.copy(alpha = 0.7f),
+ fontSize = 14.sp
+ )
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ val itemToDelete = viewModel.getItemToSave(garages)
+ onDeleteItem(itemToDelete)
+ viewModel.clearFormForNewItem()
+ showDeleteConfirmDialog = false
+ scope.launch { snackbarHostState.showSnackbar("🗑️ Item deleted successfully") }
+ },
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFEF4444)),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Icon(Icons.Default.Delete, null, modifier = Modifier.size(18.dp))
+ Spacer(Modifier.width(6.dp))
+ Text("Delete Permanently", fontWeight = FontWeight.Bold)
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showDeleteConfirmDialog = false }) {
+ Text("Cancel", color = Color.White.copy(alpha = 0.7f))
+ }
+ }
+ )
+ }
+
+ // ==================== MAIN UI ====================
+
+ Surface(modifier = Modifier.fillMaxSize(), color = Color(0xFF0A0A0A)) {
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
- .padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState())
- .padding(bottom = 80.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp)
+ .padding(bottom = 200.dp)
) {
- Row(
- modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Column {
+ // Header with gradient
+ Box(modifier = Modifier.fillMaxWidth()) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(220.dp)
+ .background(
+ Brush.verticalGradient(
+ colors = listOf(Color(0xFF6366F1), Color(0xFF8B5CF6))
+ )
+ )
+ )
+ Column(modifier = Modifier.padding(20.dp)) {
Text(
- "Create / Edit Item",
- style = MaterialTheme.typography.headlineMedium,
+ "Create Item",
+ color = Color.White,
+ fontSize = 32.sp,
fontWeight = FontWeight.Bold
)
- Row(
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- val statusIcon = if (viewModel.hasUnsavedChanges) Icons.Default.Edit else Icons.Default.Check
- val statusText = if (viewModel.hasUnsavedChanges) "Unsaved changes" else "All changes saved"
- val statusColor = if (viewModel.hasUnsavedChanges)
- MaterialTheme.colorScheme.tertiary
- else
- MaterialTheme.colorScheme.primary
- Icon(statusIcon, statusText, tint = statusColor, modifier = Modifier.size(16.dp))
- Text(statusText, style = MaterialTheme.typography.bodySmall, color = statusColor)
- }
- }
- OutlinedButton(
- onClick = { autoSaveEnabled = !autoSaveEnabled },
- colors = ButtonDefaults.outlinedButtonColors(
- contentColor = if (autoSaveEnabled)
- MaterialTheme.colorScheme.primary
- else
- MaterialTheme.colorScheme.onSurfaceVariant
- )
- ) {
- Icon(
- if (autoSaveEnabled) Icons.Default.Check else Icons.Default.Close,
- null,
- modifier = Modifier.size(16.dp)
+ Spacer(Modifier.height(8.dp))
+ Text(
+ "Add new items to your inventory",
+ color = Color.White.copy(alpha = 0.8f),
+ fontSize = 14.sp
)
- Text("Auto-save: ${if (autoSaveEnabled) "ON" else "OFF"}", modifier = Modifier.padding(start = 4.dp))
+
+ Spacer(Modifier.height(20.dp))
+
+ // Status card
+ StatusCard(viewModel = viewModel)
}
}
+ // Camera Preference Banner
AnimatedVisibility(
visible = showCameraPreferenceBanner,
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut()
) {
- Card(
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.tertiaryContainer
- ),
- modifier = Modifier.fillMaxWidth()
- ) {
- Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
- Text("📸 Auto-Camera Enabled", fontWeight = FontWeight.Bold)
- Text(
- "The camera opens automatically on 'New Item'. You can change this in Settings.",
- style = MaterialTheme.typography.bodySmall
- )
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
- TextButton({
- onSettingsChange(
- appSettings.copy(
- openCameraOnNewItem = false,
- hasShownCameraPreference = true
- )
- )
- showCameraPreferenceBanner = false
- }) {
- Text("Turn Off")
- }
- TextButton({
- onSettingsChange(appSettings.copy(hasShownCameraPreference = true))
- showCameraPreferenceBanner = false
- }) {
- Text("Got It")
- }
- }
- }
- }
- }
-
- ModernCard {
- SectionHeader("📝 Core Details")
- ModernTextField(
- value = viewModel.itemName,
- onValueChange = { viewModel.itemName = it },
- label = "Item Name *",
- leadingIcon = Icons.Default.Edit
- )
- ModernTextField(
- value = viewModel.modelNumber,
- onValueChange = { viewModel.modelNumber = it },
- label = "Model Number",
- leadingIcon = Icons.Default.ConfirmationNumber
- )
- Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
- Box(Modifier.weight(1f)) {
- DropdownField(
- "Condition",
- listOf("New", "Used", "Good", "For Parts"),
- viewModel.condition
- ) { viewModel.condition = it }
- }
- Box(Modifier.weight(1f)) {
- DropdownField(
- "Functionality",
- listOf("Fully Functional", "Partially Functional", "Not Functional", "Needs Testing"),
- viewModel.functionality
- ) { viewModel.functionality = it }
- }
- }
- ModernTextField(
- value = viewModel.description,
- onValueChange = { viewModel.description = it },
- label = "Description",
- singleLine = false,
- modifier = Modifier.height(120.dp),
- leadingIcon = Icons.AutoMirrored.Filled.Notes
+ CameraPreferenceBanner(
+ onDismiss = { showCameraPreferenceBanner = false }
)
}
- ModernCard {
- SectionHeader("📍 Location")
- DropdownField(
- "Garage *",
- garageOptions,
- viewModel.selectedGarageName
- ) {
- viewModel.selectedGarageName = it
- viewModel.selectedCabinetName = ""
- viewModel.selectedShelfName = ""
- viewModel.selectedBoxName = null
- }
- DropdownField(
- "Cabinet",
- cabinetOptions,
- viewModel.selectedCabinetName
- ) {
- viewModel.selectedCabinetName = it
- viewModel.selectedShelfName = ""
- viewModel.selectedBoxName = null
- }
- DropdownField(
- "Shelf",
- shelfOptions,
- viewModel.selectedShelfName
- ) {
- viewModel.selectedShelfName = it
- viewModel.selectedBoxName = null
- }
- DropdownField(
- "Box/Bin (Optional)",
- listOf("None") + boxOptions,
- viewModel.selectedBoxName ?: "None"
- ) {
- viewModel.selectedBoxName = if (it == "None") null else it
- }
- }
+ Spacer(Modifier.height(16.dp))
- ModernCard {
- SectionHeader("📊 Attributes")
- Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ // Main Content
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ // Core Details
+ ModernCard(title = "📝 Item Details") {
ModernTextField(
- value = viewModel.quantity,
- onValueChange = { viewModel.quantity = it },
- label = "Quantity",
- keyboardType = KeyboardType.Number,
- modifier = Modifier.weight(1f)
+ value = viewModel.itemName,
+ onValueChange = { viewModel.itemName = it },
+ label = "Item Name *",
+ leadingIcon = Icons.Default.Edit
)
ModernTextField(
- value = viewModel.weight,
- onValueChange = { viewModel.weight = it },
- label = "Weight (lbs)",
- keyboardType = KeyboardType.Decimal,
- modifier = Modifier.weight(1f)
+ value = viewModel.modelNumber,
+ onValueChange = { viewModel.modelNumber = it },
+ label = "Model Number",
+ leadingIcon = Icons.Default.ConfirmationNumber
)
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ Box(Modifier.weight(1f)) {
+ DropdownField(
+ "Condition",
+ listOf("New", "Like New", "Good", "Fair", "Poor"),
+ viewModel.condition
+ ) { viewModel.condition = it }
+ }
+ Box(Modifier.weight(1f)) {
+ DropdownField(
+ "Status",
+ listOf("Fully Functional", "Partially Functional", "Not Functional", "Needs Testing"),
+ viewModel.functionality
+ ) { viewModel.functionality = it }
+ }
+ }
}
- Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
- ModernTextField(
- value = viewModel.minPrice,
- onValueChange = { viewModel.minPrice = it },
- label = "Min Price ($)",
- keyboardType = KeyboardType.Decimal,
- modifier = Modifier.weight(1f)
- )
+
+ // Description
+ ModernCard(title = "📄 Description") {
ModernTextField(
- value = viewModel.maxPrice,
- onValueChange = { viewModel.maxPrice = it },
- label = "Max Price ($)",
- keyboardType = KeyboardType.Decimal,
- modifier = Modifier.weight(1f)
+ value = viewModel.description,
+ onValueChange = { viewModel.description = it },
+ label = "Description / Notes",
+ singleLine = false,
+ modifier = Modifier.height(120.dp),
+ leadingIcon = Icons.AutoMirrored.Filled.Notes
)
}
- Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
- Box(Modifier.weight(1f)) {
- DropdownField(
- "Size",
- listOf("Small", "Medium", "Large"),
- viewModel.sizeCategory
- ) { viewModel.sizeCategory = it }
+
+ // Location
+ ModernCard(title = "📍 Storage Location") {
+ DropdownField("Garage *", garageOptions, viewModel.selectedGarageName) {
+ viewModel.selectedGarageName = it
+ viewModel.selectedCabinetName = ""
+ viewModel.selectedShelfName = ""
+ viewModel.selectedBoxName = null
+ }
+ DropdownField("Cabinet", cabinetOptions, viewModel.selectedCabinetName) {
+ viewModel.selectedCabinetName = it
+ viewModel.selectedShelfName = ""
+ viewModel.selectedBoxName = null
+ }
+ DropdownField("Shelf", shelfOptions, viewModel.selectedShelfName) {
+ viewModel.selectedShelfName = it
+ viewModel.selectedBoxName = null
+ }
+ DropdownField(
+ "Box/Bin (Optional)",
+ listOf("None") + boxOptions,
+ viewModel.selectedBoxName ?: "None"
+ ) {
+ viewModel.selectedBoxName = if (it == "None") null else it
}
- ModernTextField(
- value = viewModel.dimensions,
- onValueChange = { viewModel.dimensions = it },
- label = "Dimensions (LxWxH)",
- modifier = Modifier.weight(1f)
- )
}
- }
- ModernCard {
- ModernTextField(
- value = viewModel.webLink,
- onValueChange = { viewModel.webLink = it },
- label = "Web Link / URL"
- )
- }
+ // Quantity & Physical Attributes
+ ModernCard(title = "📦 Quantity & Attributes") {
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ ModernTextField(
+ value = viewModel.quantity,
+ onValueChange = { viewModel.quantity = it },
+ label = "Quantity",
+ keyboardType = KeyboardType.Number,
+ modifier = Modifier.weight(1f)
+ )
+ ModernTextField(
+ value = viewModel.weight,
+ onValueChange = { viewModel.weight = it },
+ label = "Weight (lbs)",
+ keyboardType = KeyboardType.Decimal,
+ modifier = Modifier.weight(1f)
+ )
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ Box(Modifier.weight(1f)) {
+ DropdownField(
+ "Size",
+ listOf("Small", "Medium", "Large", "Extra Large"),
+ viewModel.sizeCategory
+ ) { viewModel.sizeCategory = it }
+ }
+ ModernTextField(
+ value = viewModel.dimensions,
+ onValueChange = { viewModel.dimensions = it },
+ label = "Dimensions (LxWxH)",
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
- ModernCard {
- SectionHeader("📸 Images & Recognition")
- Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- Button(
- { launchCameraWithPermissionCheck() },
- Modifier.weight(1f),
- shape = RoundedCornerShape(12.dp)
- ) { Text("📷 Camera") }
- Button(
- { galleryLauncher.launch("image/*") },
- Modifier.weight(1f),
- shape = RoundedCornerShape(12.dp)
- ) { Text("📁 Upload") }
+ // Pricing
+ ModernCard(title = "💰 Pricing") {
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ ModernTextField(
+ value = viewModel.minPrice,
+ onValueChange = { viewModel.minPrice = it },
+ label = "Min Price ($)",
+ keyboardType = KeyboardType.Decimal,
+ modifier = Modifier.weight(1f)
+ )
+ ModernTextField(
+ value = viewModel.maxPrice,
+ onValueChange = { viewModel.maxPrice = it },
+ label = "Max Price ($)",
+ keyboardType = KeyboardType.Decimal,
+ modifier = Modifier.weight(1f)
+ )
+ }
}
- Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- OutlinedButton(
- {},
- Modifier.weight(1f),
- shape = RoundedCornerShape(12.dp)
- ) { Text("📄 OCR") }
- OutlinedButton(
- {},
- Modifier.weight(1f),
- shape = RoundedCornerShape(12.dp)
- ) { Text("🤖 AI ID") }
+
+ // Web Link
+ ModernCard(title = "🔗 Product Link") {
+ ModernTextField(
+ value = viewModel.webLink,
+ onValueChange = { viewModel.webLink = it },
+ label = "Web Link / Product URL"
+ )
}
- if (viewModel.imageUris.isNotEmpty()) {
- Row(Modifier.horizontalScroll(rememberScrollState()).padding(top = 8.dp)) {
- viewModel.imageUris.forEach { uri ->
- Box(modifier = Modifier.padding(4.dp)) {
- Card(
- shape = RoundedCornerShape(12.dp),
- elevation = CardDefaults.cardElevation(4.dp)
- ) {
- Image(
- rememberAsyncImagePainter(uri),
- "Selected",
- Modifier.size(120.dp)
- )
- }
- IconButton(
- onClick = {
- viewModel.imageUris.removeAt(viewModel.imageUris.indexOf(uri))
- viewModel.checkForChanges()
- },
- modifier = Modifier
- .align(Alignment.TopEnd)
- .padding(4.dp)
- .background(Color.Black.copy(0.6f), RoundedCornerShape(8.dp))
- .size(32.dp)
- ) {
- Icon(Icons.Default.Close, "Delete image", tint = Color.White)
+
+ // Images
+ ModernCard(title = "📸 Images") {
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ Button(
+ { launchCameraWithPermissionCheck() },
+ Modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF6366F1)),
+ shape = RoundedCornerShape(12.dp),
+ contentPadding = PaddingValues(16.dp)
+ ) {
+ Icon(Icons.Default.CameraAlt, null, modifier = Modifier.size(20.dp))
+ Spacer(Modifier.width(8.dp))
+ Text("Camera", fontWeight = FontWeight.Bold)
+ }
+ Button(
+ { galleryLauncher.launch("image/*") },
+ Modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF8B5CF6)),
+ shape = RoundedCornerShape(12.dp),
+ contentPadding = PaddingValues(16.dp)
+ ) {
+ Text("📁 Upload", fontWeight = FontWeight.Bold)
+ }
+ }
+
+ if (viewModel.imageUris.isNotEmpty()) {
+ Spacer(Modifier.height(12.dp))
+ Text(
+ "${viewModel.imageUris.size} image(s) attached",
+ color = Color.White.copy(alpha = 0.6f),
+ fontSize = 12.sp
+ )
+ Row(
+ Modifier
+ .horizontalScroll(rememberScrollState())
+ .padding(top = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ viewModel.imageUris.forEach { uri ->
+ Box {
+ Card(
+ shape = RoundedCornerShape(16.dp),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Image(
+ rememberAsyncImagePainter(uri),
+ "Selected",
+ Modifier.size(120.dp)
+ )
+ }
+ IconButton(
+ onClick = {
+ viewModel.imageUris.remove(uri)
+ viewModel.checkForChanges()
+ },
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .padding(8.dp)
+ .background(Color(0xFFEF4444), CircleShape)
+ .size(32.dp)
+ ) {
+ Icon(
+ Icons.Default.Close,
+ "Delete",
+ tint = Color.White,
+ modifier = Modifier.size(18.dp)
+ )
+ }
}
}
}
}
}
+
+ Spacer(Modifier.height(20.dp))
}
}
- Card(
- modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter),
- shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
- elevation = CardDefaults.cardElevation(8.dp)
+ // Loading Overlay
+ if (isProcessingOCR || isProcessingAI) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(Color.Black.copy(alpha = 0.85f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(24.dp)
+ ) {
+ val infiniteTransition = rememberInfiniteTransition(label = "processing_pulse")
+ val scale by infiniteTransition.animateFloat(
+ initialValue = 1f,
+ targetValue = 1.2f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1000, easing = EaseInOut),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "scale"
+ )
+
+ Icon(
+ if (isProcessingOCR) Icons.Default.DocumentScanner else Icons.Default.Psychology,
+ contentDescription = null,
+ modifier = Modifier
+ .size(80.dp)
+ .graphicsLayer(scaleX = scale, scaleY = scale),
+ tint = if (isProcessingOCR) Color(0xFF6366F1) else Color(0xFF8B5CF6)
+ )
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ if (isProcessingOCR) "📸 Processing OCR..." else "🤖 Processing AI...",
+ color = Color.White,
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ if (isProcessingOCR) "Extracting text from image" else "Analyzing item details",
+ color = Color.White.copy(alpha = 0.6f),
+ fontSize = 14.sp
+ )
+ }
+
+ CircularProgressIndicator(
+ color = if (isProcessingOCR) Color(0xFF6366F1) else Color(0xFF8B5CF6),
+ modifier = Modifier.size(48.dp)
+ )
+ }
+ }
+ }
+
+ // Bottom Action Bar
+ Surface(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .fillMaxWidth(),
+ shadowElevation = 12.dp,
+ color = Color(0xFF1A1A1A)
) {
- Row(
- modifier = Modifier.fillMaxWidth().padding(16.dp),
- horizontalArrangement = Arrangement.spacedBy(12.dp)
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
) {
- Button(
- onClick = ::handleNewItemAndCamera,
- modifier = Modifier.weight(1f),
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.secondary
- ),
- shape = RoundedCornerShape(12.dp)
+ // OCR and AI buttons row
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.fillMaxWidth()
) {
- Text("➕ New Item")
+ Button(
+ onClick = { performOCR() },
+ modifier = Modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF6366F1)),
+ shape = RoundedCornerShape(14.dp),
+ contentPadding = PaddingValues(16.dp)
+ ) {
+ Icon(Icons.Default.DocumentScanner, null, modifier = Modifier.size(20.dp))
+ Spacer(Modifier.width(8.dp))
+ Text("OCR Scan", fontSize = 15.sp, fontWeight = FontWeight.Bold)
+ }
+ Button(
+ onClick = { performAI() },
+ modifier = Modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF8B5CF6)),
+ shape = RoundedCornerShape(14.dp),
+ contentPadding = PaddingValues(16.dp)
+ ) {
+ Icon(Icons.Default.Psychology, null, modifier = Modifier.size(20.dp))
+ Spacer(Modifier.width(8.dp))
+ Text("AI Fill", fontSize = 15.sp, fontWeight = FontWeight.Bold)
+ }
}
- Button(
- onClick = { saveItem() },
- modifier = Modifier.weight(1f),
- shape = RoundedCornerShape(12.dp),
- enabled = viewModel.itemName.isNotBlank()
+
+ // Main action buttons row
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.fillMaxWidth()
) {
- val scale by animateFloatAsState(
- if (viewModel.hasUnsavedChanges) 1.1f else 1f,
- label = "s"
- )
- Icon(Icons.Default.Check, "Save", modifier = Modifier.scale(scale))
- Text("Save Item", modifier = Modifier.padding(start = 4.dp))
+ Button(
+ onClick = { handleNewItemClick() },
+ modifier = Modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2A2A2A)),
+ shape = RoundedCornerShape(14.dp),
+ contentPadding = PaddingValues(16.dp)
+ ) {
+ Icon(Icons.Default.Edit, null, modifier = Modifier.size(20.dp))
+ Spacer(Modifier.width(6.dp))
+ Text("New", fontSize = 15.sp, fontWeight = FontWeight.Bold)
+ }
+ Button(
+ onClick = { saveItem() },
+ modifier = Modifier.weight(1.2f),
+ shape = RoundedCornerShape(14.dp),
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF10B981)),
+ contentPadding = PaddingValues(16.dp),
+ enabled = viewModel.itemName.isNotBlank() && viewModel.selectedGarageName.isNotBlank()
+ ) {
+ val scale by animateFloatAsState(
+ if (viewModel.hasUnsavedChanges) 1.15f else 1f,
+ label = "save_scale"
+ )
+ Icon(
+ Icons.Default.SaveAs,
+ "Save",
+ modifier = Modifier
+ .scale(scale)
+ .size(20.dp)
+ )
+ Spacer(Modifier.width(6.dp))
+ Text("Save", fontSize = 15.sp, fontWeight = FontWeight.Bold)
+ }
+ Button(
+ onClick = { showDeleteConfirmDialog = true },
+ modifier = Modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFEF4444)),
+ shape = RoundedCornerShape(14.dp),
+ contentPadding = PaddingValues(16.dp)
+ ) {
+ Icon(Icons.Default.Delete, null, modifier = Modifier.size(20.dp))
+ Spacer(Modifier.width(6.dp))
+ Text("Delete", fontSize = 15.sp, fontWeight = FontWeight.Bold)
+ }
}
}
}
+ // Snackbar at BOTTOM
SnackbarHost(
hostState = snackbarHostState,
- modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 90.dp)
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 180.dp, start = 16.dp, end = 16.dp)
) { data ->
Snackbar(
snackbarData = data,
- shape = RoundedCornerShape(12.dp),
- containerColor = MaterialTheme.colorScheme.primaryContainer,
- contentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ shape = RoundedCornerShape(14.dp),
+ containerColor = Color(0xFF2A2A2A),
+ contentColor = Color.White
)
}
}
}
}
+// ==================== COMPOSABLE COMPONENTS ====================
+
+@Composable
+private fun StatusCard(viewModel: CreateItemViewModel) {
+ val isNameEmpty = viewModel.itemName.isBlank()
+ val isLocationEmpty = viewModel.selectedGarageName.isBlank()
+
+ when {
+ isNameEmpty || isLocationEmpty -> {
+ Card(
+ colors = CardDefaults.cardColors(containerColor = Color(0xFFEF4444).copy(alpha = 0.2f)),
+ shape = RoundedCornerShape(16.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Icon(
+ Icons.Default.Warning,
+ "Warning",
+ tint = Color(0xFFEF4444),
+ modifier = Modifier.size(28.dp)
+ )
+ Column {
+ Text(
+ "⚠️ Required Fields Missing",
+ color = Color.White,
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp
+ )
+ Text(
+ if (isNameEmpty && isLocationEmpty) "Item name and location required"
+ else if (isNameEmpty) "Item name required"
+ else "Storage location required",
+ color = Color.White.copy(alpha = 0.7f),
+ fontSize = 12.sp
+ )
+ }
+ }
+ }
+ }
+ viewModel.hasUnsavedChanges -> {
+ Card(
+ colors = CardDefaults.cardColors(containerColor = Color(0xFFFBBF24).copy(alpha = 0.2f)),
+ shape = RoundedCornerShape(16.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Icon(
+ Icons.Default.Edit,
+ "Unsaved",
+ tint = Color(0xFFFBBF24),
+ modifier = Modifier.size(28.dp)
+ )
+ Column {
+ Text(
+ "📝 Unsaved Changes",
+ color = Color.White,
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp
+ )
+ Text(
+ "Don't forget to save your work",
+ color = Color.White.copy(alpha = 0.7f),
+ fontSize = 12.sp
+ )
+ }
+ }
+ }
+ }
+ else -> {
+ Card(
+ colors = CardDefaults.cardColors(containerColor = Color(0xFF10B981).copy(alpha = 0.2f)),
+ shape = RoundedCornerShape(16.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Icon(
+ Icons.Default.Check,
+ "Saved",
+ tint = Color(0xFF10B981),
+ modifier = Modifier.size(28.dp)
+ )
+ Column {
+ Text(
+ "✅ All Saved",
+ color = Color.White,
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp
+ )
+ Text(
+ "Your item is up to date",
+ color = Color.White.copy(alpha = 0.7f),
+ fontSize = 12.sp
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
@Composable
-fun ModernCard(content: @Composable ColumnScope.() -> Unit) {
+private fun CameraPreferenceBanner(
+ onDismiss: () -> Unit
+) {
Card(
- modifier = Modifier.fillMaxWidth(),
- elevation = CardDefaults.cardElevation(3.dp),
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
- ),
+ colors = CardDefaults.cardColors(containerColor = Color(0xFF6366F1).copy(alpha = 0.2f)),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
shape = RoundedCornerShape(16.dp)
) {
- Column(
- modifier = Modifier.padding(20.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp),
- content = content
- )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ Icons.Default.Info,
+ null,
+ tint = Color(0xFF6366F1),
+ modifier = Modifier.size(24.dp)
+ )
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ "📸 Auto-Camera Active",
+ fontWeight = FontWeight.Bold,
+ color = Color.White,
+ fontSize = 14.sp
+ )
+ Text(
+ "Camera opens on 'New Item'",
+ fontSize = 12.sp,
+ color = Color.White.copy(alpha = 0.7f)
+ )
+ }
+ IconButton(onClick = onDismiss) {
+ Icon(Icons.Default.Close, null, tint = Color.White, modifier = Modifier.size(20.dp))
+ }
+ }
}
}
@Composable
-fun SectionHeader(text: String) {
- Text(
- text,
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.primary
- )
+fun ModernCard(
+ title: String,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(4.dp),
+ colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A)),
+ shape = RoundedCornerShape(20.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ title,
+ color = Color(0xFF6366F1),
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp
+ )
+ content()
+ }
+ }
}
@Composable
@@ -615,15 +1217,20 @@ fun ModernTextField(
OutlinedTextField(
value = value,
onValueChange = onValueChange,
- label = { Text(label) },
- leadingIcon = leadingIcon?.let { { Icon(it, null) } },
+ label = { Text(label, fontSize = 13.sp) },
+ leadingIcon = leadingIcon?.let { { Icon(it, null, modifier = Modifier.size(20.dp), tint = Color(0xFF6366F1)) } },
modifier = modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
singleLine = singleLine,
shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors(
- focusedBorderColor = MaterialTheme.colorScheme.primary,
- unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)
+ focusedBorderColor = Color(0xFF6366F1),
+ unfocusedBorderColor = Color(0xFF3A3A3A),
+ focusedTextColor = Color.White,
+ unfocusedTextColor = Color.White,
+ cursorColor = Color(0xFF6366F1),
+ focusedLabelColor = Color(0xFF6366F1),
+ unfocusedLabelColor = Color.White.copy(alpha = 0.6f)
)
)
}
@@ -639,39 +1246,94 @@ fun DropdownField(
var isExpanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = isExpanded,
- onExpandedChange = { isExpanded = it }
+ onExpandedChange = { isExpanded = !isExpanded }
) {
OutlinedTextField(
value = selectedValue.ifEmpty { "Select..." },
onValueChange = {},
readOnly = true,
- label = { Text(label) },
+ label = { Text(label, fontSize = 13.sp) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) },
- modifier = Modifier.fillMaxWidth().menuAnchor(),
shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors(
- focusedBorderColor = MaterialTheme.colorScheme.primary,
- unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)
- )
+ focusedBorderColor = Color(0xFF6366F1),
+ unfocusedBorderColor = Color(0xFF3A3A3A),
+ focusedTextColor = Color.White,
+ unfocusedTextColor = Color.White,
+ focusedLabelColor = Color(0xFF6366F1),
+ unfocusedLabelColor = Color.White.copy(alpha = 0.6f)
+ ),
+ modifier = Modifier
+ .fillMaxWidth()
+ .menuAnchor()
)
+
ExposedDropdownMenu(
expanded = isExpanded,
- onDismissRequest = { isExpanded = false }
+ onDismissRequest = { isExpanded = false },
+ modifier = Modifier.background(Color(0xFF2A2A2A))
) {
options.forEach { option ->
DropdownMenuItem(
- text = { Text(option) },
+ text = { Text(option, fontSize = 13.sp, color = Color.White) },
onClick = {
onValueChange(option)
isExpanded = false
- }
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding
)
}
}
}
}
-fun createImageUri(context: Context): Uri {
- val imageFile = File(context.cacheDir, "${UUID.randomUUID()}.jpg")
- return FileProvider.getUriForFile(context, "${context.packageName}.provider", imageFile)
+@Composable
+fun ModernAlertDialog(
+ onDismissRequest: () -> Unit,
+ icon: ImageVector,
+ iconTint: Color,
+ title: String,
+ content: @Composable () -> Unit,
+ confirmButton: @Composable () -> Unit,
+ dismissButton: @Composable () -> Unit
+) {
+ AlertDialog(
+ onDismissRequest = onDismissRequest,
+ icon = {
+ Icon(
+ icon,
+ contentDescription = null,
+ tint = iconTint,
+ modifier = Modifier.size(32.dp)
+ )
+ },
+ title = {
+ Text(title, color = Color.White, fontWeight = FontWeight.Bold, fontSize = 20.sp)
+ },
+ text = content,
+ confirmButton = confirmButton,
+ dismissButton = dismissButton,
+ containerColor = Color(0xFF1A1A1A),
+ shape = RoundedCornerShape(20.dp)
+ )
+}
+
+@Composable
+fun InfoRow(label: String, value: String) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ label,
+ fontWeight = FontWeight.Bold,
+ color = Color.White.copy(alpha = 0.6f),
+ fontSize = 13.sp
+ )
+ Text(
+ value,
+ color = Color.White,
+ fontSize = 13.sp
+ )
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/CreateItemVewModel.kt b/app/src/main/java/com/samuel/inventorymanager/screens/CreateItemVewModel.kt
index 4b0a2f5..719f881 100644
--- a/app/src/main/java/com/samuel/inventorymanager/screens/CreateItemVewModel.kt
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/CreateItemVewModel.kt
@@ -1,60 +1,209 @@
package com.samuel.inventorymanager.screens
+import android.app.Application
import android.net.Uri
+import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
-import androidx.lifecycle.ViewModel
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.samuel.inventorymanager.services.OCRService
+import kotlinx.coroutines.launch
import java.util.UUID
-class CreateItemViewModel : ViewModel() {
+// --- IMPORTANT: Import your Data Models here ---
+// If your Item/Garage classes are in 'com.samuel.inventorymanager.data', keep these.
+// If they are in 'models', change 'data' to 'models'.
+//import com.samuel.inventorymanager.data.Garage
+//import com.samuel.inventorymanager.data.Item
- // Form fields - all mutable state
+// --- Helper Data Class for AI Results ---
+data class AIAnalysisResult(
+ val itemName: String? = null,
+ val confidence: Double = 0.0,
+ val modelNumber: String? = null,
+ val description: String? = null,
+ val estimatedPrice: Double? = null,
+ val condition: String? = null,
+ val sizeCategory: String? = null,
+ val dimensions: String? = null,
+ val rawText: String? = null
+)
+
+class CreateItemViewModel(
+ application: Application,
+ private val ocrService: OCRService
+) : AndroidViewModel(application) {
+
+ // --- Core Item Data ---
+ var currentItem: Item? by mutableStateOf(null)
+
+ // --- UI Form State ---
var itemName by mutableStateOf("")
var modelNumber by mutableStateOf("")
var description by mutableStateOf("")
var webLink by mutableStateOf("")
- var condition by mutableStateOf("")
- var functionality by mutableStateOf("")
- var quantity by mutableStateOf("")
+ var condition by mutableStateOf("Good")
+ var functionality by mutableStateOf("Working")
+ var quantity by mutableStateOf("1")
var minPrice by mutableStateOf("")
var maxPrice by mutableStateOf("")
var weight by mutableStateOf("")
- var sizeCategory by mutableStateOf("")
+ var sizeCategory by mutableStateOf("Medium")
var dimensions by mutableStateOf("")
- // Location fields
+ // --- Location State ---
var selectedGarageName by mutableStateOf("")
var selectedCabinetName by mutableStateOf("")
var selectedShelfName by mutableStateOf("")
var selectedBoxName by mutableStateOf(null)
- // Images
+ // --- Image & AI State ---
val imageUris = mutableStateListOf()
+ var isProcessing by mutableStateOf(false)
+ var aiAnalysisResult by mutableStateOf(null)
+ var showAIPreview by mutableStateOf(false)
- // Change tracking
+ // --- Change Tracking ---
var hasUnsavedChanges by mutableStateOf(false)
private set
- private var lastSavedState: Int = 0
+ private var lastSavedHash: Int = 0
init {
- lastSavedState = getCurrentStateHash()
+ markAsSaved()
}
- fun checkForChanges() {
- hasUnsavedChanges = getCurrentStateHash() != lastSavedState
+ // =================================================================================
+ // 1. SMART AI/OCR FUNCTIONALITY
+ // =================================================================================
+
+ /**
+ * UI should call this when an image is captured.
+ */
+ @Suppress("unused") // Called from UI
+ fun analyzeImage(imageUri: Uri) {
+ isProcessing = true
+
+ viewModelScope.launch {
+ try {
+ // 1. Run OCR
+ val result = ocrService.performOCR(imageUri)
+
+ // 2. Smart Parse
+ val analyzedData = smartParseOCRText(result.text)
+
+ // 3. Update Preview
+ aiAnalysisResult = analyzedData
+
+ // Add to images if new
+ if (!imageUris.contains(imageUri)) {
+ imageUris.add(imageUri)
+ }
+
+ showAIPreview = true
+ } catch (e: Exception) {
+ Log.e("CreateItemVM", "AI Analysis failed: ${e.message}")
+ } finally {
+ isProcessing = false
+ }
+ }
+ }
+
+
+ /**
+ * Helper to parse raw text into structured data.
+ */
+ private fun smartParseOCRText(rawText: String): AIAnalysisResult {
+ val lines = rawText.lines()
+
+ // Guess Model Number (uppercase + numbers mixed, >3 chars)
+ val modelRegex = Regex("\\b(?=.*[A-Z])(?=.*\\d)[A-Z\\d-]{4,}\\b")
+ val possibleModel = lines.firstNotNullOfOrNull { line ->
+ modelRegex.find(line)?.value
+ }
+
+ // Guess Dimensions (Num x Num or NumxNum)
+ val dimRegex = Regex("\\d+(\\.\\d+)?\\s*[xX]\\s*\\d+(\\.\\d+)?(\\s*[xX]\\s*\\d+(\\.\\d+)?)?")
+ val possibleDimensions = lines.firstNotNullOfOrNull { line ->
+ dimRegex.find(line)?.value
+ }
+
+ // Guess Price
+ val priceRegex = Regex("\\$\\s*([0-9,]+(\\.\\d{2})?)")
+ val priceString = lines.firstNotNullOfOrNull { line ->
+ priceRegex.find(line)?.groupValues?.get(1)
+ }?.replace(",", "")
+ val priceVal = priceString?.toDoubleOrNull()
+
+ // Name (simple guess: first reasonably long line without a '$')
+ val possibleName = lines.firstOrNull { it.length > 4 && !it.contains("$") }
+
+ return AIAnalysisResult(
+ itemName = possibleName ?: "",
+ confidence = 0.5, // Add confidence estimate
+ modelNumber = possibleModel,
+ description = rawText.take(200),
+ estimatedPrice = priceVal,
+ dimensions = possibleDimensions,
+ rawText = rawText,
+ condition = null,
+ sizeCategory = null
+ )
+ }
+
+ /**
+ * Called when user confirms AI preview.
+ */
+ @Suppress("unused") // Called from UI
+ fun applyAIResultToForm(result: AIAnalysisResult) {
+ if (itemName.isBlank()) result.itemName?.let { itemName = it }
+ if (modelNumber.isBlank()) result.modelNumber?.let { modelNumber = it }
+
+ // Combine description logic
+ if (!result.rawText.isNullOrBlank()) {
+ description = if (description.isBlank()) {
+ result.rawText
+ } else {
+ "$description\n\n--- Scanned Data ---\n${result.rawText}"
+ }
+ }
+
+ if (dimensions.isBlank()) result.dimensions?.let { dimensions = it }
+
+ result.estimatedPrice?.let { p ->
+ if (minPrice.isBlank()) minPrice = p.toString()
+ if (maxPrice.isBlank()) maxPrice = (p * 1.2).toString()
+ }
+
+ checkForChanges()
+ showAIPreview = false
}
+
+ // =================================================================================
+ // 2. CRUD LOGIC
+ // =================================================================================
+
private fun getCurrentStateHash(): Int {
return listOf(
itemName, modelNumber, description, webLink, condition, functionality,
quantity, minPrice, maxPrice, weight, sizeCategory, dimensions,
selectedGarageName, selectedCabinetName, selectedShelfName, selectedBoxName,
- imageUris.size
+ imageUris.toList().map { it.toString() }
).hashCode()
}
+ fun checkForChanges() {
+ hasUnsavedChanges = getCurrentStateHash() != lastSavedHash
+ }
+
+ fun markAsSaved() {
+ lastSavedHash = getCurrentStateHash()
+ hasUnsavedChanges = false
+ }
+
fun getItemToSave(garages: List): Item {
val garage = garages.find { it.name == selectedGarageName }
val cabinet = garage?.cabinets?.find { it.name == selectedCabinetName }
@@ -62,7 +211,7 @@ class CreateItemViewModel : ViewModel() {
val box = shelf?.boxes?.find { it.name == selectedBoxName }
return Item(
- id = UUID.randomUUID().toString(),
+ id = currentItem?.id ?: UUID.randomUUID().toString(),
name = itemName,
modelNumber = modelNumber.ifBlank { null },
description = description.ifBlank { null },
@@ -83,39 +232,35 @@ class CreateItemViewModel : ViewModel() {
)
}
- fun markAsSaved() {
- lastSavedState = getCurrentStateHash()
- hasUnsavedChanges = false
- }
-
- fun clearFormForNewItem(garages: List) {
+ fun clearFormForNewItem() {
+ currentItem = null
itemName = ""
modelNumber = ""
description = ""
webLink = ""
- condition = ""
- functionality = ""
- quantity = ""
+ condition = "Good"
+ functionality = "Working"
+ quantity = "1"
minPrice = ""
maxPrice = ""
weight = ""
- sizeCategory = ""
+ sizeCategory = "Medium"
dimensions = ""
imageUris.clear()
- // Keep location if valid, otherwise reset
- if (garages.none { it.name == selectedGarageName }) {
- selectedGarageName = ""
- selectedCabinetName = ""
- selectedShelfName = ""
- selectedBoxName = null
- }
+ // Reset Location state logic (Optional: keep location if rapid adding?)
+ selectedGarageName = ""
+ selectedCabinetName = ""
+ selectedShelfName = ""
+ selectedBoxName = null
- hasUnsavedChanges = false
- lastSavedState = getCurrentStateHash()
+ aiAnalysisResult = null
+ markAsSaved()
}
fun loadItemForEditing(item: Item, garages: List) {
+ currentItem = item
+
val garage = garages.find { it.id == item.garageId }
val cabinet = garage?.cabinets?.find { it.id == item.cabinetId }
val shelf = cabinet?.shelves?.find { it.id == item.shelfId }
@@ -140,9 +285,10 @@ class CreateItemViewModel : ViewModel() {
selectedBoxName = box?.name
imageUris.clear()
+ // Use .toUri() from core-ktx or standard Uri.parse
imageUris.addAll(item.images.map { Uri.parse(it) })
- hasUnsavedChanges = false
- lastSavedState = getCurrentStateHash()
+ markAsSaved()
}
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/DataModels.kt b/app/src/main/java/com/samuel/inventorymanager/screens/DataModels.kt
index e37e203..5e8248b 100644
--- a/app/src/main/java/com/samuel/inventorymanager/screens/DataModels.kt
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/DataModels.kt
@@ -1,31 +1,51 @@
package com.samuel.inventorymanager.screens
+import java.util.UUID
+
// --- Core Data Structures ---
data class Item(
- val id: String,
+ val id: String = UUID.randomUUID().toString(),
val name: String,
- val modelNumber: String?,
- val description: String?,
- val webLink: String?,
- val condition: String,
- val functionality: String,
+ val modelNumber: String? = null,
+ val description: String? = null,
+ val webLink: String? = null,
+ val condition: String = "Good",
+ val functionality: String = "Working",
val garageId: String,
val cabinetId: String,
val shelfId: String,
- val boxId: String?,
- val quantity: Int,
- val minPrice: Double?,
- val maxPrice: Double?,
- val weight: Double?,
- val sizeCategory: String,
- val dimensions: String?,
- val images: List
+ val boxId: String? = null,
+ val quantity: Int = 1,
+ val minPrice: Double? = null,
+ val maxPrice: Double? = null,
+ val weight: Double? = null,
+ val sizeCategory: String = "Medium",
+ val dimensions: String? = null,
+ val images: List = emptyList()
+)
+
+data class Box(
+ val id: String = UUID.randomUUID().toString(),
+ val name: String
+)
+
+data class Shelf(
+ val id: String = UUID.randomUUID().toString(),
+ val name: String,
+ val boxes: MutableList = mutableListOf()
)
-data class Box(val id: String, val name: String)
-data class Shelf(val id: String, val name: String, val boxes: List)
-data class Cabinet(val id: String, val name: String, val shelves: List)
-data class Garage(val id: String, val name: String, val cabinets: List)
+data class Cabinet(
+ val id: String = UUID.randomUUID().toString(),
+ val name: String,
+ val shelves: MutableList = mutableListOf()
+)
+
+data class Garage(
+ val id: String = UUID.randomUUID().toString(),
+ val name: String,
+ val cabinets: MutableList = mutableListOf()
+)
// --- History Tracking ---
sealed class HistoryAction {
@@ -38,17 +58,17 @@ sealed class HistoryAction {
}
data class HistoryEntry(
- val id: String,
+ val id: String = UUID.randomUUID().toString(),
val itemId: String,
val itemName: String,
- val action: HistoryAction,
+ val actionType: String,
val description: String,
val timestamp: Long = System.currentTimeMillis()
)
// --- App Data Bundle for Saving/Loading ---
data class AppData(
- val garages: List,
- val items: List
- ,
- val history: List
+ val garages: List = emptyList(),
+ val items: List
- = emptyList(),
+ val history: List = emptyList()
)
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/GoogleSyncScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/GoogleSyncScreen.kt
new file mode 100644
index 0000000..93f821b
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/GoogleSyncScreen.kt
@@ -0,0 +1,890 @@
+@file:Suppress("DEPRECATION")
+
+package com.samuel.inventorymanager.screens
+
+import android.content.Context
+import android.util.Log
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+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.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.CheckCircle
+import androidx.compose.material.icons.filled.CloudDone
+import androidx.compose.material.icons.filled.CloudDownload
+import androidx.compose.material.icons.filled.CloudOff
+import androidx.compose.material.icons.filled.CloudSync
+import androidx.compose.material.icons.filled.CloudUpload
+import androidx.compose.material.icons.filled.DeleteForever
+import androidx.compose.material.icons.filled.Devices
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.Login
+import androidx.compose.material.icons.filled.Logout
+import androidx.compose.material.icons.filled.Security
+import androidx.compose.material.icons.filled.Sync
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Switch
+import androidx.compose.material3.SwitchDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+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.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.google.android.gms.auth.api.signin.GoogleSignIn
+import com.google.android.gms.auth.api.signin.GoogleSignInOptions
+import com.google.android.gms.common.api.Scope
+import com.google.api.client.extensions.android.http.AndroidHttp
+import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
+import com.google.api.client.http.ByteArrayContent
+import com.google.api.client.json.gson.GsonFactory
+import com.google.api.services.drive.Drive
+import com.google.api.services.drive.DriveScopes
+import com.google.gson.Gson
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.ByteArrayOutputStream
+
+private const val BACKUP_FILE_NAME = "inventory_manager_backup.json"
+private const val AUTO_SAVE_INTERVAL = 30000L // 30 seconds
+
+@Composable
+fun GoogleSyncScreen(
+ garages: List,
+ items: List
- ,
+ history: List,
+ onDataRestored: (AppData) -> Unit
+) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+
+ var driveService by remember { mutableStateOf(null) }
+ var userEmail by remember { mutableStateOf(null) }
+ var isLoading by remember { mutableStateOf(false) }
+ var statusMessage by remember { mutableStateOf("") }
+ var lastSyncTime by remember { mutableStateOf(null) }
+ var autoSaveEnabled by remember { mutableStateOf(true) }
+ var showDeleteDialog by remember { mutableStateOf(false) }
+ var showSignOutDialog by remember { mutableStateOf(false) }
+
+ // Check if already signed in
+ LaunchedEffect(Unit) {
+ val account = GoogleSignIn.getLastSignedInAccount(context)
+ if (account != null) {
+ userEmail = account.email
+ val credential = GoogleAccountCredential.usingOAuth2(
+ context,
+ listOf(DriveScopes.DRIVE_APPDATA)
+ ).setSelectedAccount(account.account)
+
+ driveService = Drive.Builder(
+ AndroidHttp.newCompatibleTransport(),
+ GsonFactory.getDefaultInstance(),
+ credential
+ ).setApplicationName("Inventory Manager").build()
+ }
+ }
+
+ // Auto-save loop
+ LaunchedEffect(autoSaveEnabled, driveService, garages.size, items.size, history.size) {
+ if (autoSaveEnabled && driveService != null) {
+ while (true) {
+ delay(AUTO_SAVE_INTERVAL)
+ try {
+ val data = AppData(garages, items, history)
+ uploadToGoogleDrive(context, driveService!!, data)
+ lastSyncTime = System.currentTimeMillis()
+ statusMessage = "✅ Auto-saved"
+ } catch (e: Exception) {
+ Log.e("GoogleSync", "Auto-save failed", e)
+ }
+ }
+ }
+ }
+
+ val signInLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
+ try {
+ val account = task.result
+ userEmail = account.email
+
+ val credential = GoogleAccountCredential.usingOAuth2(
+ context,
+ listOf(DriveScopes.DRIVE_APPDATA)
+ ).setSelectedAccount(account.account)
+
+ driveService = Drive.Builder(
+ AndroidHttp.newCompatibleTransport(),
+ GsonFactory.getDefaultInstance(),
+ credential
+ ).setApplicationName("Inventory Manager").build()
+
+ statusMessage = "✅ Connected successfully"
+ } catch (e: Exception) {
+ Log.e("GoogleSync", "Sign-in failed", e)
+ statusMessage = "❌ Sign-in failed"
+ }
+ }
+
+ val gso = remember {
+ GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
+ .requestEmail()
+ .requestScopes(Scope(DriveScopes.DRIVE_APPDATA))
+ .build()
+ }
+ val googleSignInClient = remember { GoogleSignIn.getClient(context, gso) }
+
+ // Dialogs
+ if (showDeleteDialog) {
+ AlertDialog(
+ onDismissRequest = { showDeleteDialog = false },
+ icon = { Icon(Icons.Default.DeleteForever, null, tint = Color(0xFFEF4444)) },
+ title = { Text("Delete All Cloud Data?", fontWeight = FontWeight.Bold) },
+ text = {
+ Text("This will permanently delete ALL your inventory data from Google. Your local data will remain unchanged.\n\nThis cannot be undone!")
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ scope.launch {
+ isLoading = true
+ try {
+ deleteFromGoogleDrive(driveService!!)
+ statusMessage = "🗑️ Cloud data deleted"
+ } catch (e: Exception) {
+ statusMessage = "❌ Delete failed"
+ }
+ isLoading = false
+ showDeleteDialog = false
+ }
+ },
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFEF4444))
+ ) {
+ Text("Delete Everything")
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showDeleteDialog = false }) {
+ Text("Cancel")
+ }
+ }
+ )
+ }
+
+ if (showSignOutDialog) {
+ AlertDialog(
+ onDismissRequest = { showSignOutDialog = false },
+ icon = { Icon(Icons.Default.Logout, null) },
+ title = { Text("Sign Out?", fontWeight = FontWeight.Bold) },
+ text = { Text("Your data will remain in the cloud and locally. You can sign in again anytime.") },
+ confirmButton = {
+ Button(
+ onClick = {
+ googleSignInClient.signOut()
+ driveService = null
+ userEmail = null
+ autoSaveEnabled = false
+ statusMessage = "👋 Signed out"
+ showSignOutDialog = false
+ }
+ ) {
+ Text("Sign Out")
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showSignOutDialog = false }) {
+ Text("Cancel")
+ }
+ }
+ )
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color(0xFF0A0A0A))
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(bottom = 20.dp)
+ ) {
+ // Header
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp)
+ .background(
+ Brush.verticalGradient(
+ colors = listOf(Color(0xFF4285F4), Color(0xFF34A853))
+ )
+ )
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(24.dp),
+ verticalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ Icons.Default.CloudSync,
+ null,
+ modifier = Modifier.size(48.dp),
+ tint = Color.White
+ )
+ Spacer(Modifier.height(12.dp))
+ Text(
+ "Google Sync",
+ color = Color.White,
+ fontSize = 32.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "Automatic cloud backup",
+ color = Color.White.copy(alpha = 0.9f),
+ fontSize = 16.sp
+ )
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Spacer(Modifier.height(20.dp))
+
+ // Connection Status Card
+ AnimatedVisibility(
+ visible = driveService != null,
+ enter = fadeIn() + expandVertically(),
+ exit = fadeOut() + shrinkVertically()
+ ) {
+ ConnectedStatusCard(
+ userEmail = userEmail ?: "",
+ lastSyncTime = lastSyncTime,
+ autoSaveEnabled = autoSaveEnabled
+ )
+ }
+
+ AnimatedVisibility(
+ visible = driveService == null,
+ enter = fadeIn() + expandVertically(),
+ exit = fadeOut() + shrinkVertically()
+ ) {
+ DisconnectedStatusCard()
+ }
+
+ // Status Message
+ AnimatedVisibility(
+ visible = statusMessage.isNotEmpty(),
+ enter = fadeIn() + slideInVertically(),
+ exit = fadeOut() + slideOutVertically()
+ ) {
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Icon(
+ Icons.Default.Info,
+ null,
+ tint = Color(0xFF60A5FA),
+ modifier = Modifier.size(24.dp)
+ )
+ Text(
+ statusMessage,
+ color = Color.White,
+ fontSize = 14.sp
+ )
+ }
+ }
+ }
+
+ // Main Actions
+ if (driveService == null) {
+ // Sign In Button
+ Button(
+ onClick = { signInLauncher.launch(googleSignInClient.signInIntent) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color(0xFF4285F4)
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Icon(Icons.Default.Login, null)
+ Spacer(Modifier.width(12.dp))
+ Text("Connect Google Account", fontSize = 16.sp, fontWeight = FontWeight.Bold)
+ }
+ } else {
+ // Auto-save Toggle
+ SyncOptionCard(
+ icon = if (autoSaveEnabled) Icons.Default.CloudDone else Icons.Default.CloudOff,
+ title = "Auto-Save",
+ description = if (autoSaveEnabled) "Saves every 30 seconds" else "Disabled",
+ enabled = autoSaveEnabled,
+ onToggle = { autoSaveEnabled = !autoSaveEnabled }
+ )
+
+ // Manual Actions
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ ActionButton(
+ icon = Icons.Default.CloudUpload,
+ text = "Backup Now",
+ color = Color(0xFF10B981),
+ modifier = Modifier.weight(1f),
+ enabled = !isLoading
+ ) {
+ scope.launch {
+ isLoading = true
+ try {
+ val data = AppData(garages, items, history)
+ uploadToGoogleDrive(context, driveService!!, data)
+ lastSyncTime = System.currentTimeMillis()
+ statusMessage = "✅ Backup complete"
+ } catch (e: Exception) {
+ statusMessage = "❌ Backup failed"
+ }
+ isLoading = false
+ }
+ }
+
+ ActionButton(
+ icon = Icons.Default.CloudDownload,
+ text = "Restore",
+ color = Color(0xFF8B5CF6),
+ modifier = Modifier.weight(1f),
+ enabled = !isLoading
+ ) {
+ scope.launch {
+ isLoading = true
+ try {
+ val data = downloadFromGoogleDrive(driveService!!)
+ if (data != null) {
+ onDataRestored(data)
+ statusMessage = "✅ Data restored"
+ } else {
+ statusMessage = "⚠️ No backup found"
+ }
+ } catch (e: Exception) {
+ statusMessage = "❌ Restore failed"
+ }
+ isLoading = false
+ }
+ }
+ }
+
+ Spacer(Modifier.height(8.dp))
+
+ // Danger Zone
+ DangerZoneCard(
+ onSignOut = { showSignOutDialog = true },
+ onDeleteAll = { showDeleteDialog = true }
+ )
+ }
+
+ // Info Cards
+ InfoSection()
+ }
+ }
+
+ // Loading Overlay
+ if (isLoading) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Black.copy(alpha = 0.7f)),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(
+ color = Color(0xFF4285F4),
+ modifier = Modifier.size(64.dp)
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun ConnectedStatusCard(
+ userEmail: String,
+ lastSyncTime: Long?,
+ autoSaveEnabled: Boolean
+) {
+ val infiniteTransition = rememberInfiniteTransition(label = "pulse")
+ val alpha by infiniteTransition.animateFloat(
+ initialValue = 0.3f,
+ targetValue = 1f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1000),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "alpha"
+ )
+
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF10B981).copy(alpha = 0.15f)
+ ),
+ shape = RoundedCornerShape(20.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .size(56.dp)
+ .clip(CircleShape)
+ .background(Color(0xFF10B981).copy(alpha = if (autoSaveEnabled) alpha else 1f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.Default.CheckCircle,
+ null,
+ tint = Color.White,
+ modifier = Modifier.size(32.dp)
+ )
+ }
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ "Connected",
+ color = Color(0xFF10B981),
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ userEmail,
+ color = Color.White.copy(alpha = 0.7f),
+ fontSize = 14.sp
+ )
+ if (lastSyncTime != null) {
+ val timeAgo = getTimeAgo(lastSyncTime)
+ Text(
+ "Last sync: $timeAgo",
+ color = Color.White.copy(alpha = 0.5f),
+ fontSize = 12.sp
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun DisconnectedStatusCard() {
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFFFBBF24).copy(alpha = 0.15f)
+ ),
+ shape = RoundedCornerShape(20.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .size(56.dp)
+ .clip(CircleShape)
+ .background(Color(0xFFFBBF24)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.Default.CloudOff,
+ null,
+ tint = Color.White,
+ modifier = Modifier.size(32.dp)
+ )
+ }
+ Column {
+ Text(
+ "Not Connected",
+ color = Color(0xFFFBBF24),
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "Sign in to enable cloud backup",
+ color = Color.White.copy(alpha = 0.7f),
+ fontSize = 14.sp
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun SyncOptionCard(
+ icon: ImageVector,
+ title: String,
+ description: String,
+ enabled: Boolean,
+ onToggle: () -> Unit
+) {
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ icon,
+ null,
+ tint = if (enabled) Color(0xFF10B981) else Color.White.copy(alpha = 0.5f),
+ modifier = Modifier.size(32.dp)
+ )
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ title,
+ color = Color.White,
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ description,
+ color = Color.White.copy(alpha = 0.6f),
+ fontSize = 13.sp
+ )
+ }
+ Switch(
+ checked = enabled,
+ onCheckedChange = { onToggle() },
+ colors = SwitchDefaults.colors(
+ checkedThumbColor = Color.White,
+ checkedTrackColor = Color(0xFF10B981)
+ )
+ )
+ }
+ }
+}
+
+@Composable
+private fun ActionButton(
+ icon: ImageVector,
+ text: String,
+ color: Color,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ onClick: () -> Unit
+) {
+ Button(
+ onClick = onClick,
+ modifier = modifier.height(80.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = color.copy(alpha = 0.15f),
+ disabledContainerColor = Color.Gray.copy(alpha = 0.15f)
+ ),
+ shape = RoundedCornerShape(16.dp),
+ enabled = enabled
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(
+ icon,
+ null,
+ tint = if (enabled) color else Color.Gray,
+ modifier = Modifier.size(28.dp)
+ )
+ Text(
+ text,
+ color = if (enabled) Color.White else Color.Gray,
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+}
+
+@Composable
+private fun DangerZoneCard(
+ onSignOut: () -> Unit,
+ onDeleteAll: () -> Unit
+) {
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFFEF4444).copy(alpha = 0.1f)
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text(
+ "⚠️ Danger Zone",
+ color = Color(0xFFEF4444),
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Bold
+ )
+
+ OutlinedButton(
+ onClick = onSignOut,
+ modifier = Modifier.fillMaxWidth(),
+ colors = ButtonDefaults.outlinedButtonColors(
+ contentColor = Color.White
+ ),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Icon(Icons.Default.Logout, null)
+ Spacer(Modifier.width(8.dp))
+ Text("Sign Out")
+ }
+
+ OutlinedButton(
+ onClick = onDeleteAll,
+ modifier = Modifier.fillMaxWidth(),
+ colors = ButtonDefaults.outlinedButtonColors(
+ contentColor = Color(0xFFEF4444)
+ ),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Icon(Icons.Default.DeleteForever, null)
+ Spacer(Modifier.width(8.dp))
+ Text("Delete All Cloud Data")
+ }
+ }
+ }
+}
+
+@Composable
+private fun InfoSection() {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Text(
+ "How it works",
+ color = Color.White,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold
+ )
+
+ InfoCard(
+ icon = Icons.Default.Security,
+ title = "Secure & Private",
+ description = "Your data is encrypted and stored in your Google account"
+ )
+
+ InfoCard(
+ icon = Icons.Default.Sync,
+ title = "Automatic Backup",
+ description = "Auto-saves every 30 seconds when enabled"
+ )
+
+ InfoCard(
+ icon = Icons.Default.Devices,
+ title = "Access Anywhere",
+ description = "Restore your data on any device with your Google account"
+ )
+ }
+}
+
+@Composable
+private fun InfoCard(icon: ImageVector, title: String, description: String) {
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ ),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Icon(
+ icon,
+ null,
+ tint = Color(0xFF60A5FA),
+ modifier = Modifier.size(24.dp)
+ )
+ Column {
+ Text(
+ title,
+ color = Color.White,
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ description,
+ color = Color.White.copy(alpha = 0.6f),
+ fontSize = 12.sp
+ )
+ }
+ }
+ }
+}
+
+// Helper functions
+private fun getTimeAgo(timestamp: Long): String {
+ val diff = System.currentTimeMillis() - timestamp
+ val seconds = diff / 1000
+ val minutes = seconds / 60
+ val hours = minutes / 60
+
+ return when {
+ seconds < 60 -> "just now"
+ minutes < 60 -> "$minutes min ago"
+ hours < 24 -> "$hours hr ago"
+ else -> "${hours / 24} days ago"
+ }
+}
+
+private suspend fun uploadToGoogleDrive(context: Context, driveService: Drive, data: AppData) {
+ withContext(Dispatchers.IO) {
+ try {
+ val json = Gson().toJson(data)
+ val content = ByteArrayContent("application/json", json.toByteArray())
+
+ // Check if file exists
+ val result = driveService.files().list()
+ .setSpaces("appDataFolder")
+ .setQ("name='$BACKUP_FILE_NAME'")
+ .execute()
+
+ if (result.files.isNullOrEmpty()) {
+ // Create new file
+ val fileMetadata = com.google.api.services.drive.model.File().apply {
+ name = BACKUP_FILE_NAME
+ parents = listOf("appDataFolder")
+ }
+ driveService.files().create(fileMetadata, content).execute()
+ } else {
+ // Update existing file
+ val fileId = result.files[0].id
+ driveService.files().update(fileId, null, content).execute()
+ }
+
+ Log.d("GoogleSync", "Upload successful")
+ } catch (e: Exception) {
+ Log.e("GoogleSync", "Upload failed", e)
+ throw e
+ }
+ }
+}
+
+private suspend fun downloadFromGoogleDrive(driveService: Drive): AppData? {
+ return withContext(Dispatchers.IO) {
+ try {
+ val result = driveService.files().list()
+ .setSpaces("appDataFolder")
+ .setQ("name='$BACKUP_FILE_NAME'")
+ .execute()
+
+ if (result.files.isNullOrEmpty()) {
+ return@withContext null
+ }
+
+ val fileId = result.files[0].id
+ val outputStream = ByteArrayOutputStream()
+ driveService.files().get(fileId).executeMediaAndDownloadTo(outputStream)
+
+ val json = String(outputStream.toByteArray())
+ Gson().fromJson(json, AppData::class.java)
+ } catch (e: Exception) {
+ Log.e("GoogleSync", "Download failed", e)
+ null
+ }
+ }
+}
+
+private suspend fun deleteFromGoogleDrive(driveService: Drive) {
+ withContext(Dispatchers.IO) {
+ try {
+ val result = driveService.files().list()
+ .setSpaces("appDataFolder")
+ .setQ("name='$BACKUP_FILE_NAME'")
+ .execute()
+
+ if (!result.files.isNullOrEmpty()) {
+ val fileId = result.files[0].id
+ driveService.files().delete(fileId).execute()
+ Log.d("GoogleSync", "Delete successful")
+ }
+ } catch (e: Exception) {
+ Log.e("GoogleSync", "Delete failed", e)
+ throw e
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/HelpScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/HelpScreen.kt
index 948aed0..b359c7d 100644
--- a/app/src/main/java/com/samuel/inventorymanager/screens/HelpScreen.kt
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/HelpScreen.kt
@@ -1,7 +1,6 @@
package com.samuel.inventorymanager.screens
import android.content.Intent
-import android.net.Uri
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.clickable
@@ -29,7 +28,7 @@ import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.ShoppingBag
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.Divider
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -47,6 +46,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.core.net.toUri
@Composable
fun HelpScreen() {
@@ -60,7 +60,9 @@ fun HelpScreen() {
item {
// --- HEADER ---
Column(
- modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("❓", fontSize = 56.sp)
@@ -132,7 +134,7 @@ fun HelpScreen() {
// --- DATA & BACKUP ---
item {
CollapsibleHelpSection(
- title = "Data, Backup & Sync",
+ title = "Data, Backup & (Sync COMING SOON...)",
icon = Icons.Default.SdStorage
) {
HelpContentBlock(
@@ -181,11 +183,13 @@ fun HelpScreen() {
Spacer(Modifier.width(12.dp))
Text("App Information", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
}
- InfoRow("Version:", "2.0 (Native)")
- InfoRow("Platform:", "Android (Jetpack Compose)")
- InfoRow("Data Storage:", "Local Device Storage")
+ // FIX: Renamed InfoRow to AppInfoRow to solve overload ambiguity
+ AppInfoRow("Version:", "1.0 (FIRST)")
+ AppInfoRow("Platform:", "Android (Jetpack Compose)")
+ AppInfoRow("Data Storage:", "Local Device Storage")
- Divider(modifier = Modifier.padding(vertical = 8.dp))
+ // FIX: Replaced deprecated Divider with HorizontalDivider
+ HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
// Contact Section
ContactRow(
@@ -193,7 +197,8 @@ fun HelpScreen() {
title = "GitHub of Parminder",
subtitle = "github.com/JohnJackson12",
onClick = {
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/JohnJackson12"))
+ // FIX: Use .toUri() KTX extension
+ val intent = Intent(Intent.ACTION_VIEW, "https://github.com/JohnJackson12".toUri())
context.startActivity(intent)
}
)
@@ -203,7 +208,8 @@ fun HelpScreen() {
title = "GitHub of Samuel",
subtitle = "github.com/SamS34",
onClick = {
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/SamS34"))
+ // FIX: Use .toUri() KTX extension
+ val intent = Intent(Intent.ACTION_VIEW, "https://github.com/SamS34".toUri())
context.startActivity(intent)
}
)
@@ -214,7 +220,8 @@ fun HelpScreen() {
subtitle = "parminder.nz@gmail.com",
onClick = {
val intent = Intent(Intent.ACTION_SENDTO).apply {
- data = Uri.parse("mailto:parminder.nz@gmail.com")
+ // FIX: Use .toUri() KTX extension
+ data = "mailto:parminder.nz@gmail.com".toUri()
}
context.startActivity(intent)
}
@@ -226,7 +233,8 @@ fun HelpScreen() {
subtitle = "sam.of.s34@gmail.com",
onClick = {
val intent = Intent(Intent.ACTION_SENDTO).apply {
- data = Uri.parse("mailto:sam.of.s34@gmail.com")
+ // FIX: Use .toUri() KTX extension
+ data = "mailto:sam.of.s34@gmail.com".toUri()
}
context.startActivity(intent)
}
@@ -239,7 +247,7 @@ fun HelpScreen() {
// ======================================================================
-// HELPER COMPOSABLES
+// Helper Composables
// These are the reusable building blocks that make the screen work! ✨
// ======================================================================
@@ -291,7 +299,8 @@ private fun CollapsibleHelpSection(
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
- Divider()
+ // FIX: Replaced deprecated Divider with HorizontalDivider
+ HorizontalDivider()
Spacer(Modifier.height(4.dp))
content()
}
@@ -332,7 +341,9 @@ private fun ContactRow(
onClick: () -> Unit
) {
Row(
- modifier = Modifier.fillMaxWidth().clickable(onClick = onClick),
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick),
verticalAlignment = Alignment.CenterVertically
) {
Icon(icon, null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
@@ -352,9 +363,10 @@ private fun ContactRow(
/**
* Displays a simple "Label: Value" row for app information.
+ * FIX: Renamed from InfoRow to AppInfoRow to resolve compiler ambiguity.
*/
@Composable
-private fun InfoRow(label: String, value: String) {
+private fun AppInfoRow(label: String, value: String) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text(
text = label,
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/HistoryScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/HistoryScreen.kt
index 27bb1fd..117cfa1 100644
--- a/app/src/main/java/com/samuel/inventorymanager/screens/HistoryScreen.kt
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/HistoryScreen.kt
@@ -1,5 +1,6 @@
package com.samuel.inventorymanager.screens
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -9,194 +10,342 @@ 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.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.automirrored.filled.Login
+import androidx.compose.material.icons.automirrored.filled.Logout
+import androidx.compose.material.icons.automirrored.filled.Sort
import androidx.compose.material.icons.filled.AddCircle
-import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material.icons.filled.ArrowDownward
+import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.DeleteForever
import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.HistoryToggleOff
-import androidx.compose.material.icons.filled.Login
-import androidx.compose.material.icons.filled.Logout
+import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.SwapVert
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
+import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
+
+
+sealed class ActionType(
+ val key: String,
+ val label: String,
+ val icon: androidx.compose.ui.graphics.vector.ImageVector,
+ val color: Color
+) {
+ object Added : ActionType("Added", "Added", Icons.Filled.AddCircle, Color(0xFF10B981))
+ object Updated : ActionType("Updated", "Updated", Icons.Filled.Edit, Color(0xFFF59E0B))
+ object Removed : ActionType("Removed", "Removed", Icons.Filled.Delete, Color(0xFFEF4444))
+ object QuantityChanged : ActionType("QuantityChanged", "Qty Changed", Icons.Filled.SwapVert, Color(0xFF8B5CF6))
+ object CheckedOut : ActionType("CheckedOut", "Checked Out", Icons.AutoMirrored.Filled.Logout, Color(0xFFEF4444))
+ object CheckedIn : ActionType("CheckedIn", "Checked In", Icons.AutoMirrored.Filled.Login, Color(0xFF10B981))
+ data class Unknown(val raw: String) : ActionType(raw, if (raw.isBlank()) "Unknown" else raw, Icons.Filled.History, Color(0xFF6B7280))
+
+ companion object {
+ fun fromRaw(raw: String): ActionType = when (raw) {
+ Added.key -> Added
+ Updated.key -> Updated
+ Removed.key -> Removed
+ QuantityChanged.key -> QuantityChanged
+ CheckedOut.key -> CheckedOut
+ CheckedIn.key -> CheckedIn
+ else -> Unknown(raw)
+ }
+
+ val filterableTypes = listOf(Added, Updated, Removed, QuantityChanged, CheckedOut, CheckedIn)
+ }
+}
+
+enum class HistorySortField { DATE, NAME }
+enum class HistorySortDirection { ASCENDING, DESCENDING }
+data class HistorySort(val field: HistorySortField, val direction: HistorySortDirection)
+
+data class HistoryUiState(
+ val searchQuery: String = "",
+ val selectedActionKey: String? = null,
+ val sortField: HistorySortField = HistorySortField.DATE,
+ val sortDirection: HistorySortDirection = HistorySortDirection.DESCENDING,
+ val pendingDeleteId: String? = null
+)
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HistoryScreen(
history: List,
items: List
- ,
onItemClick: (Item) -> Unit,
- onClearHistory: () -> Unit
+ onClearHistory: () -> Unit,
+ onDeleteHistoryEntry: (HistoryEntry) -> Unit
) {
- var searchQuery by remember { mutableStateOf("") }
- var selectedActionType by remember { mutableStateOf(null) }
-
- // Filter history based on search and selected action type
- val filteredHistory = remember(searchQuery, selectedActionType, history) {
- history.filter { entry ->
- val matchesAction = selectedActionType == null ||
- when (entry.action) {
- is HistoryAction.Added -> selectedActionType == ActionType.ADDED
- is HistoryAction.Updated -> selectedActionType == ActionType.UPDATED
- is HistoryAction.Removed -> selectedActionType == ActionType.REMOVED
- is HistoryAction.QuantityChanged -> selectedActionType == ActionType.QUANTITY_CHANGED
- is HistoryAction.CheckedOut -> selectedActionType == ActionType.CHECKED_OUT
- is HistoryAction.CheckedIn -> selectedActionType == ActionType.CHECKED_IN
- }
+ val localHistory = remember(history) { mutableStateListOf().apply { addAll(history) } }
+ var uiState by remember { mutableStateOf(HistoryUiState()) }
+ val snackbarHostState = remember { SnackbarHostState() }
+ val scope = rememberCoroutineScope()
- val matchesSearch = searchQuery.isBlank() ||
- entry.itemName.contains(searchQuery, ignoreCase = true) ||
- entry.description.contains(searchQuery, ignoreCase = true)
+ val selectedActionType = uiState.selectedActionKey?.let { ActionType.fromRaw(it) }
+ val filteredSorted = remember(localHistory, uiState) {
+ localHistory.filter { e ->
+ (selectedActionType == null || ActionType.fromRaw(e.actionType).key == selectedActionType.key) &&
+ (uiState.searchQuery.isBlank() || e.itemName.contains(uiState.searchQuery, ignoreCase = true) || e.description.contains(uiState.searchQuery, ignoreCase = true))
+ }.sortedWith { a, b ->
+ val cmp = when (uiState.sortField) {
+ HistorySortField.DATE -> a.timestamp.compareTo(b.timestamp)
+ HistorySortField.NAME -> a.itemName.lowercase().compareTo(b.itemName.lowercase())
+ }
+ if (uiState.sortDirection == HistorySortDirection.ASCENDING) cmp else -cmp
+ }
+ }
- matchesAction && matchesSearch
+ fun clearAllWithUndo() {
+ val backup = localHistory.toList()
+ localHistory.clear()
+ scope.launch {
+ val result = snackbarHostState.showSnackbar("History cleared", actionLabel = "Undo", duration = SnackbarDuration.Short)
+ if (result == SnackbarResult.ActionPerformed) localHistory.addAll(backup) else onClearHistory()
}
}
- Column(modifier = Modifier.fillMaxSize()) {
- HistoryHeader(
- searchQuery = searchQuery,
- onSearchQueryChange = { searchQuery = it },
- selectedActionType = selectedActionType,
- onActionSelect = { actionType ->
- selectedActionType = if (selectedActionType == actionType) null else actionType
- },
- onClearHistory = onClearHistory
- )
+ fun deleteWithUndo(entry: HistoryEntry) {
+ localHistory.remove(entry)
+ onDeleteHistoryEntry(entry)
+ scope.launch {
+ val result = snackbarHostState.showSnackbar("Entry deleted", actionLabel = "Undo", duration = SnackbarDuration.Short)
+ if (result == SnackbarResult.ActionPerformed) localHistory.add(entry)
+ }
+ }
- if (filteredHistory.isEmpty()) {
- EmptyHistoryState()
- } else {
- LazyColumn(
+ Scaffold(topBar = { HistoryTopBar() }, snackbarHost = { SnackbarHost(snackbarHostState) }) { padding ->
+ Column(
+ Modifier.padding(padding).fillMaxSize()
+ ) {
+ HistoryHeader(
+ searchQuery = uiState.searchQuery,
+ selectedActionType = selectedActionType,
+ sort = HistorySort(uiState.sortField, uiState.sortDirection),
+ historyCount = filteredSorted.size,
+ totalCount = localHistory.size,
+ onSearchQueryChange = { uiState = uiState.copy(searchQuery = it) },
+ onActionSelect = { type -> uiState = uiState.copy(selectedActionKey = if (uiState.selectedActionKey == type.key) null else type.key) },
+ onSortFieldChange = { uiState = uiState.copy(sortField = it) },
+ onSortDirectionToggle = {
+ val newDir = if (uiState.sortDirection == HistorySortDirection.DESCENDING) HistorySortDirection.ASCENDING else HistorySortDirection.DESCENDING
+ uiState = uiState.copy(sortDirection = newDir)
+ },
+ onClearHistoryClick = { if (localHistory.isNotEmpty()) clearAllWithUndo() }
+ )
+ Spacer(Modifier.height(12.dp))
+ if (filteredSorted.isEmpty()) EmptyHistoryState(Modifier.fillMaxSize().padding(24.dp)) else LazyColumn(
modifier = Modifier.fillMaxSize(),
- contentPadding = PaddingValues(16.dp),
- verticalArrangement = Arrangement.spacedBy(12.dp)
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ contentPadding = PaddingValues(horizontal = 12.dp, vertical = 10.dp)
) {
- items(filteredHistory, key = { it.id }) { entry ->
+ items(filteredSorted, key = { it.id }) { entry ->
HistoryItemCard(
entry = entry,
- onClick = {
- items.find { it.id == entry.itemId }?.let { item ->
- onItemClick(item)
- }
- }
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { items.find { it.id == entry.itemId }?.let(onItemClick) },
+ onDeleteClick = { deleteWithUndo(entry) }
)
}
}
}
}
+
+ uiState.pendingDeleteId?.let { id ->
+ localHistory.find { it.id == id }?.let { entry ->
+ ConfirmDeleteDialog(
+ entry = entry,
+ onDismiss = { uiState = uiState.copy(pendingDeleteId = null) },
+ onConfirm = {
+ uiState = uiState.copy(pendingDeleteId = null)
+ deleteWithUndo(entry)
+ }
+ )
+ }
+ }
}
-// Helper enum to simplify filtering
-enum class ActionType {
- ADDED, UPDATED, REMOVED, QUANTITY_CHANGED, CHECKED_OUT, CHECKED_IN
+@Composable
+private fun HistoryTopBar() {
+ Surface(tonalElevation = 2.dp, shadowElevation = 2.dp) {
+ Row(
+ Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 10.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Filled.History, null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(26.dp))
+ Spacer(Modifier.width(12.dp))
+ Column {
+ Text("History", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
+ Text("Track changes to your items", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ }
+ }
+ // Remove or add meaningful menu here if desired.
+ }
+ }
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun HistoryHeader(
searchQuery: String,
- onSearchQueryChange: (String) -> Unit,
selectedActionType: ActionType?,
+ sort: HistorySort,
+ historyCount: Int,
+ totalCount: Int,
+ onSearchQueryChange: (String) -> Unit,
onActionSelect: (ActionType) -> Unit,
- onClearHistory: () -> Unit
+ onSortFieldChange: (HistorySortField) -> Unit,
+ onSortDirectionToggle: () -> Unit,
+ onClearHistoryClick: () -> Unit,
) {
Surface(
- modifier = Modifier.fillMaxWidth(),
- shadowElevation = 4.dp
+ Modifier.fillMaxWidth(),
+ tonalElevation = 4.dp,
+ shape = RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp),
+ color = MaterialTheme.colorScheme.surfaceVariant
) {
- Column(
- modifier = Modifier.padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp)
- ) {
+ Column(Modifier.padding(16.dp)) {
+ Row(
+ Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ "$historyCount item${if (historyCount == 1) "" else "s"} shown" + if (historyCount != totalCount) ", total $totalCount" else "",
+ style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold)
+ )
+ TextButton(
+ onClick = onClearHistoryClick,
+ enabled = totalCount > 0,
+ colors = ButtonDefaults.textButtonColors(
+ contentColor = MaterialTheme.colorScheme.error,
+ disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
+ )
+ ) {
+ Icon(Icons.Filled.DeleteForever, null)
+ Spacer(Modifier.width(4.dp))
+ Text("Clear All")
+ }
+ }
+ Spacer(Modifier.height(12.dp))
OutlinedTextField(
value = searchQuery,
onValueChange = onSearchQueryChange,
modifier = Modifier.fillMaxWidth(),
- placeholder = { Text("Search by item name or change...") },
- leadingIcon = { Icon(Icons.Default.Search, null) },
- trailingIcon = {
- if (searchQuery.isNotEmpty()) {
- IconButton({ onSearchQueryChange("") }) {
- Icon(Icons.Default.Clear, "Clear")
- }
- }
- },
- shape = RoundedCornerShape(12.dp)
+ placeholder = { Text("Search item or description...") },
+ singleLine = true,
+ leadingIcon = { Icon(Icons.Filled.Search, null) },
+ shape = RoundedCornerShape(12.dp),
)
-
+ Spacer(Modifier.height(12.dp))
+ Text("Filter by action", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
) {
- Row(
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- modifier = Modifier.weight(1f)
- ) {
- FilterChip(
- selected = selectedActionType == ActionType.ADDED,
- onClick = { onActionSelect(ActionType.ADDED) },
- label = { Text("Added") },
- leadingIcon = {
- Icon(Icons.Default.Add, null, modifier = Modifier.size(18.dp))
- }
- )
+ ActionType.filterableTypes.forEach { type ->
FilterChip(
- selected = selectedActionType == ActionType.UPDATED,
- onClick = { onActionSelect(ActionType.UPDATED) },
- label = { Text("Updated") },
- leadingIcon = {
- Icon(Icons.Default.Edit, null, modifier = Modifier.size(18.dp))
- }
- )
- FilterChip(
- selected = selectedActionType == ActionType.REMOVED,
- onClick = { onActionSelect(ActionType.REMOVED) },
- label = { Text("Removed") },
- leadingIcon = {
- Icon(Icons.Default.Delete, null, modifier = Modifier.size(18.dp))
- }
+ selected = selectedActionType?.key == type.key,
+ onClick = { onActionSelect(type) },
+ label = { Text(type.label) },
+ leadingIcon = { Icon(type.icon, null) },
+ colors = FilterChipDefaults.filterChipColors(
+ selectedContainerColor = type.color.copy(alpha = 0.2f),
+ selectedLabelColor = type.color,
+ selectedLeadingIconColor = type.color,
+ ),
)
}
- IconButton(onClick = onClearHistory) {
+ }
+ Spacer(Modifier.height(14.dp))
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text("Sort by:", style = MaterialTheme.typography.labelMedium)
+ var expanded by remember { mutableStateOf(false) }
+ Box {
+ TextButton(
+ onClick = { expanded = true },
+ modifier = Modifier.height(36.dp),
+ contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp)
+ ) {
+ Text(
+ when (sort.field) {
+ HistorySortField.DATE -> "Date"
+ HistorySortField.NAME -> "Name"
+ },
+ modifier = Modifier.padding(end = 4.dp)
+ )
+ Icon(Icons.AutoMirrored.Filled.Sort, contentDescription = "Sort icon", modifier = Modifier.size(16.dp))
+ }
+ DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
+ DropdownMenuItem(text = { Text("Date") }, onClick = { expanded = false; onSortFieldChange(HistorySortField.DATE) })
+ DropdownMenuItem(text = { Text("Name") }, onClick = { expanded = false; onSortFieldChange(HistorySortField.NAME) })
+ }
+ }
+ IconButton(
+ onClick = onSortDirectionToggle,
+ modifier = Modifier.size(36.dp)
+ ) {
Icon(
- Icons.Default.DeleteForever,
- "Clear History",
- tint = MaterialTheme.colorScheme.error
+ imageVector = if (sort.direction == HistorySortDirection.DESCENDING) Icons.Filled.ArrowDownward else Icons.Filled.ArrowUpward,
+ contentDescription = "Toggle sort direction"
)
}
}
@@ -205,146 +354,120 @@ private fun HistoryHeader(
}
@Composable
-private fun HistoryItemCard(entry: HistoryEntry, onClick: () -> Unit) {
- val actionDetails = when (entry.action) {
- is HistoryAction.Added -> ActionDetails(
- Icons.Default.AddCircle,
- "Added",
- MaterialTheme.colorScheme.primary
- )
- is HistoryAction.Updated -> ActionDetails(
- Icons.Default.Edit,
- "Updated",
- Color(0xFFF59E0B)
- )
- is HistoryAction.Removed -> ActionDetails(
- Icons.Default.Delete,
- "Removed",
- MaterialTheme.colorScheme.error
- )
- is HistoryAction.QuantityChanged -> {
- val qtyChange = entry.action as HistoryAction.QuantityChanged
- ActionDetails(
- Icons.Default.SwapVert,
- "Qty: ${qtyChange.oldQuantity} → ${qtyChange.newQuantity}",
- Color(0xFF8B5CF6)
- )
- }
- is HistoryAction.CheckedOut -> {
- val checkout = entry.action as HistoryAction.CheckedOut
- ActionDetails(
- Icons.Default.Logout,
- "Checked Out (${checkout.userId})",
- Color(0xFFEF4444)
- )
- }
- is HistoryAction.CheckedIn -> {
- val checkin = entry.action as HistoryAction.CheckedIn
- ActionDetails(
- Icons.Default.Login,
- "Checked In (${checkin.userId})",
- Color(0xFF10B981)
- )
- }
- }
-
+private fun HistoryItemCard(
+ entry: HistoryEntry,
+ onClick: () -> Unit,
+ onDeleteClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ elevation: Dp = 2.dp,
+) {
+ val details = remember(entry.actionType) { detailsFor(entry) }
+ var menuExpanded by remember { mutableStateOf(false) }
Card(
- modifier = Modifier.fillMaxWidth().clickable(onClick = onClick),
- elevation = CardDefaults.cardElevation(2.dp)
+ modifier = modifier.clickable(onClick = onClick),
+ elevation = CardDefaults.cardElevation(elevation),
+ shape = RoundedCornerShape(14.dp),
) {
Row(
- modifier = Modifier.padding(16.dp),
- verticalAlignment = Alignment.CenterVertically
+ Modifier.padding(horizontal = 14.dp, vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
) {
- Icon(
- imageVector = actionDetails.icon,
- contentDescription = actionDetails.label,
- modifier = Modifier.size(32.dp),
- tint = actionDetails.color
- )
-
- Spacer(Modifier.width(16.dp))
-
- Column(modifier = Modifier.weight(1f)) {
- Text(
- text = entry.itemName,
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Bold,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- Text(
- text = entry.description,
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- Text(
- text = actionDetails.label,
- style = MaterialTheme.typography.bodySmall,
- color = actionDetails.color,
- fontWeight = FontWeight.Medium
- )
+ Box(
+ Modifier.size(44.dp).background(details.color.copy(alpha = 0.16f), CircleShape),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(details.icon, details.label, tint = details.color, modifier = Modifier.size(22.dp))
+ }
+ Spacer(Modifier.width(12.dp))
+ Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ Row(
+ Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(entry.itemName, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text(formatTimestamp(entry.timestamp), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ }
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Text(details.label, style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, color = details.color)
+ Text("•", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.alpha(0.7f))
+ Text("ID: ${entry.id.take(8)}", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ }
+ if (entry.description.isNotBlank()) {
+ Text(entry.description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 2, overflow = TextOverflow.Ellipsis)
+ }
+ }
+ Box {
+ IconButton(onClick = { menuExpanded = true }) {
+ Icon(Icons.Filled.MoreVert, "More actions")
+ }
+ DropdownMenu(expanded = menuExpanded, onDismissRequest = { menuExpanded = false }) {
+ DropdownMenuItem(text = { Text("View item") }, onClick = { menuExpanded = false; onClick() })
+ DropdownMenuItem(
+ text = { Text("Delete entry") },
+ onClick = { menuExpanded = false; onDeleteClick() },
+ leadingIcon = { Icon(Icons.Filled.Delete, null, tint = MaterialTheme.colorScheme.error) }
+ )
+ }
}
-
- Spacer(Modifier.width(8.dp))
-
- Text(
- text = formatTimestamp(entry.timestamp),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
- )
}
}
}
-private data class ActionDetails(
- val icon: ImageVector,
- val label: String,
- val color: Color
-)
-
@Composable
-private fun EmptyHistoryState() {
- Box(
- modifier = Modifier.fillMaxSize().padding(16.dp),
- contentAlignment = Alignment.Center
- ) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- Icon(
- Icons.Default.HistoryToggleOff,
- "No History",
- modifier = Modifier.size(80.dp),
- tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
- )
- Text(
- "No History Yet",
- style = MaterialTheme.typography.headlineSmall
- )
- Text(
- "Changes you make to items and locations will appear here.",
- style = MaterialTheme.typography.bodyMedium,
- textAlign = TextAlign.Center,
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
- )
+private fun EmptyHistoryState(modifier: Modifier = Modifier) {
+ Box(modifier, contentAlignment = Alignment.Center) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(18.dp)) {
+ Box(
+ Modifier.size(88.dp).background(MaterialTheme.colorScheme.primary.copy(alpha = 0.16f), CircleShape),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(Icons.Filled.HistoryToggleOff, "No history", tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(52.dp).alpha(0.9f))
+ }
+ Text("No history yet", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
+ Text("Changes you make to items, quantities, and check-in/out will appear here.", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
+@Composable
+private fun ConfirmDeleteDialog(
+ entry: HistoryEntry,
+ onDismiss: () -> Unit,
+ onConfirm: () -> Unit,
+) {
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ icon = { Icon(Icons.Filled.Delete, null, tint = MaterialTheme.colorScheme.error) },
+ title = { Text("Delete this entry?") },
+ text = { Text("Remove the history record for “${entry.itemName}”? The item itself stays in inventory.") },
+ confirmButton = {
+ TextButton(onClick = onConfirm) { Text("Delete", color = MaterialTheme.colorScheme.error) }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) { Text("Cancel") }
+ },
+ )
+}
+
+private fun detailsFor(entry: HistoryEntry): ActionDetails {
+ val type = ActionType.fromRaw(entry.actionType)
+ return ActionDetails(type.icon, type.label, type.color)
+}
+
+private data class ActionDetails(val icon: ImageVector, val label: String, val color: Color)
+
private fun formatTimestamp(timestamp: Long): String {
val now = System.currentTimeMillis()
val diff = now - timestamp
-
return when {
- diff < 60_000 -> "Just now"
- diff < 3600_000 -> "${diff / 60_000}m ago"
- diff < 86400_000 -> "${diff / 3600_000}h ago"
- diff < 604800_000 -> "${diff / 86400_000}d ago"
- else -> {
- val format = SimpleDateFormat("MMM d", Locale.getDefault())
- format.format(Date(timestamp))
- }
+ diff < 60_000L -> "Just now"
+ diff < 3_600_000L -> "${diff / 60_000L}m ago"
+ diff < 86_400_000L -> "${diff / 3_600_000L}h ago"
+ diff < 7L * 86_400_000L -> "${diff / 86_400_000L}d ago"
+ else -> SimpleDateFormat("MMM d", Locale.getDefault()).format(Date(timestamp))
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/ImageEditScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/ImageEditScreen.kt
new file mode 100644
index 0000000..c44a651
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/ImageEditScreen.kt
@@ -0,0 +1,843 @@
+package com.samuel.inventorymanager.screens
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.ColorMatrix
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.Matrix
+import android.net.Uri
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectTransformGestures
+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.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowForward
+import androidx.compose.material.icons.filled.AutoAwesome
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.CropFree
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.Psychology
+import androidx.compose.material.icons.filled.RotateRight
+import androidx.compose.material.icons.filled.Tune
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+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.draw.clip
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.zIndex
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+enum class ProcessingChoice { MANUAL, AI_OCR }
+enum class EditTool { CROP, ROTATE, ADJUST }
+
+@Composable
+fun ImageEditScreen(
+ imageUri: Uri,
+ onNext: (Bitmap, ProcessingChoice) -> Unit,
+ onCancel: () -> Unit
+) {
+ val context = LocalContext.current
+
+ var bitmap by remember { mutableStateOf(null) }
+ var rotation by remember { mutableFloatStateOf(0f) }
+ var brightness by remember { mutableFloatStateOf(0f) }
+ var contrast by remember { mutableFloatStateOf(1f) }
+ var currentTool by remember { mutableStateOf(EditTool.CROP) }
+ var showChoiceDialog by remember { mutableStateOf(false) }
+
+ // Load bitmap once and keep it
+ LaunchedEffect(imageUri) {
+ withContext(Dispatchers.IO) {
+ bitmap = try {
+ context.contentResolver.openInputStream(imageUri)?.use {
+ BitmapFactory.decodeStream(it)
+ }
+ } catch (e: Exception) {
+ null
+ }
+ }
+ }
+
+ Box(modifier = Modifier.fillMaxSize()) {
+ bitmap?.let { bmp ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ Brush.verticalGradient(
+ colors = listOf(
+ Color(0xFF0F172A),
+ Color(0xFF1E293B)
+ )
+ )
+ )
+ ) {
+ // Top Bar
+ ModernTopBar(
+ onCancel = onCancel,
+ onNext = { showChoiceDialog = true }
+ )
+
+ // Image Preview
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .padding(16.dp)
+ .clip(RoundedCornerShape(24.dp)),
+ contentAlignment = Alignment.Center
+ ) {
+ EditableImagePreview(
+ bitmap = bmp,
+ rotation = rotation,
+ brightness = brightness,
+ contrast = contrast,
+ currentTool = currentTool
+ )
+ }
+
+ // Tools Panel
+ ModernToolsPanel(
+ currentTool = currentTool,
+ onToolChange = { currentTool = it },
+ rotation = rotation,
+ onRotationChange = { rotation = it },
+ brightness = brightness,
+ onBrightnessChange = { brightness = it },
+ contrast = contrast,
+ onContrastChange = { contrast = it }
+ )
+ }
+ } ?: LoadingScreen()
+
+ // Choice Dialog
+ if (showChoiceDialog) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .zIndex(100f)
+ .background(Color.Black.copy(alpha = 0.5f))
+ ) {
+ ProcessingChoiceDialog(
+ onDismiss = { showChoiceDialog = false },
+ onChoice = { choice ->
+ bitmap?.let { bmp ->
+ val processedBitmap = applyAllEdits(bmp, rotation, brightness, contrast)
+ onNext(processedBitmap, choice)
+ }
+ showChoiceDialog = false
+ }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun ModernTopBar(onCancel: () -> Unit, onNext: () -> Unit) {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .zIndex(10f),
+ color = Color(0xFF1E293B).copy(alpha = 0.95f),
+ shadowElevation = 8.dp
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ IconButton(
+ onClick = onCancel,
+ modifier = Modifier
+ .size(48.dp)
+ .background(Color.White.copy(alpha = 0.1f), CircleShape)
+ ) {
+ Icon(
+ Icons.Default.Close,
+ null,
+ tint = Color.White,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+
+ Text(
+ "✨ Edit Photo",
+ color = Color.White,
+ fontSize = 22.sp,
+ fontWeight = FontWeight.Bold
+ )
+
+ Button(
+ onClick = onNext,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color(0xFF8B5CF6)
+ ),
+ shape = RoundedCornerShape(24.dp),
+ modifier = Modifier.height(48.dp)
+ ) {
+ Text("Next", fontWeight = FontWeight.Bold, fontSize = 16.sp)
+ Spacer(Modifier.width(8.dp))
+ Icon(Icons.Default.ArrowForward, null, modifier = Modifier.size(20.dp))
+ }
+ }
+ }
+}
+
+@Composable
+fun EditableImagePreview(
+ bitmap: Bitmap,
+ rotation: Float,
+ brightness: Float,
+ contrast: Float,
+ currentTool: EditTool
+) {
+ var scale by remember { mutableFloatStateOf(1f) }
+ var offset by remember { mutableStateOf(Offset.Zero) }
+
+ val filteredBitmap by remember {
+ derivedStateOf {
+ applyImageFilters(bitmap, brightness, contrast).asImageBitmap()
+ }
+ }
+
+ Card(
+ modifier = Modifier.fillMaxSize(),
+ shape = RoundedCornerShape(24.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Color.Black.copy(alpha = 0.3f)
+ ),
+ elevation = CardDefaults.cardElevation(8.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .clip(RoundedCornerShape(24.dp)),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize(0.9f)
+ .clip(RoundedCornerShape(16.dp))
+ ) {
+ Image(
+ bitmap = filteredBitmap,
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxSize()
+ .graphicsLayer(
+ rotationZ = rotation,
+ scaleX = scale,
+ scaleY = scale,
+ translationX = offset.x,
+ translationY = offset.y
+ )
+ .pointerInput(Unit) {
+ detectTransformGestures { _, pan, zoom, _ ->
+ scale = (scale * zoom).coerceIn(0.5f, 3f)
+ val maxOffset = 500f
+ offset = Offset(
+ (offset.x + pan.x).coerceIn(-maxOffset, maxOffset),
+ (offset.y + pan.y).coerceIn(-maxOffset, maxOffset)
+ )
+ }
+ },
+ contentScale = ContentScale.Fit
+ )
+ }
+
+ if (currentTool == EditTool.CROP) {
+ Box(
+ modifier = Modifier.fillMaxSize(0.85f),
+ contentAlignment = Alignment.Center
+ ) {
+ CropGridOverlay()
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun CropGridOverlay() {
+ Canvas(modifier = Modifier.fillMaxSize()) {
+ val gridColor = Color.White.copy(alpha = 0.8f)
+ val strokeWidth = 3f
+
+ drawRect(
+ color = gridColor,
+ size = size,
+ style = androidx.compose.ui.graphics.drawscope.Stroke(width = strokeWidth)
+ )
+
+ for (i in 1..2) {
+ val x = size.width * i / 3
+ drawLine(
+ color = gridColor,
+ start = Offset(x, 0f),
+ end = Offset(x, size.height),
+ strokeWidth = strokeWidth
+ )
+ }
+
+ for (i in 1..2) {
+ val y = size.height * i / 3
+ drawLine(
+ color = gridColor,
+ start = Offset(0f, y),
+ end = Offset(size.width, y),
+ strokeWidth = strokeWidth
+ )
+ }
+
+ val handleSize = 40f
+ val cornerColor = Color(0xFF8B5CF6)
+
+ drawLine(cornerColor, Offset(0f, 0f), Offset(handleSize, 0f), strokeWidth * 2)
+ drawLine(cornerColor, Offset(0f, 0f), Offset(0f, handleSize), strokeWidth * 2)
+
+ drawLine(cornerColor, Offset(size.width - handleSize, 0f), Offset(size.width, 0f), strokeWidth * 2)
+ drawLine(cornerColor, Offset(size.width, 0f), Offset(size.width, handleSize), strokeWidth * 2)
+
+ drawLine(cornerColor, Offset(0f, size.height - handleSize), Offset(0f, size.height), strokeWidth * 2)
+ drawLine(cornerColor, Offset(0f, size.height), Offset(handleSize, size.height), strokeWidth * 2)
+
+ drawLine(cornerColor, Offset(size.width, size.height - handleSize), Offset(size.width, size.height), strokeWidth * 2)
+ drawLine(cornerColor, Offset(size.width - handleSize, size.height), Offset(size.width, size.height), strokeWidth * 2)
+ }
+}
+
+@Composable
+fun ModernToolsPanel(
+ currentTool: EditTool,
+ onToolChange: (EditTool) -> Unit,
+ rotation: Float,
+ onRotationChange: (Float) -> Unit,
+ brightness: Float,
+ onBrightnessChange: (Float) -> Unit,
+ contrast: Float,
+ onContrastChange: (Float) -> Unit
+) {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .zIndex(10f),
+ color = Color(0xFF1E293B).copy(alpha = 0.95f),
+ shadowElevation = 16.dp
+ ) {
+ Column(
+ modifier = Modifier.padding(24.dp),
+ verticalArrangement = Arrangement.spacedBy(20.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ EditTool.entries.forEach { tool ->
+ ModernToolButton(
+ tool = tool,
+ isSelected = currentTool == tool,
+ onClick = { onToolChange(tool) }
+ )
+ }
+ }
+
+ AnimatedContent(
+ targetState = currentTool,
+ label = "tool_controls",
+ transitionSpec = {
+ fadeIn() + slideInVertically() togetherWith fadeOut() + slideOutVertically()
+ }
+ ) { tool ->
+ when (tool) {
+ EditTool.CROP -> {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF8B5CF6).copy(alpha = 0.1f)
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Icon(
+ Icons.Default.Info,
+ null,
+ tint = Color(0xFF8B5CF6),
+ modifier = Modifier.size(24.dp)
+ )
+ Text(
+ "Pinch to zoom • Drag to reposition",
+ color = Color.White,
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Medium
+ )
+ }
+ }
+ }
+ EditTool.ROTATE -> {
+ ModernSliderControl(
+ label = "Rotation",
+ value = rotation,
+ onValueChange = onRotationChange,
+ valueRange = -180f..180f,
+ displayValue = "${rotation.toInt()}°",
+ color = Color(0xFF10B981)
+ )
+ }
+ EditTool.ADJUST -> {
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ ModernSliderControl(
+ label = "Brightness",
+ value = brightness,
+ onValueChange = onBrightnessChange,
+ valueRange = -1f..1f,
+ displayValue = "${(brightness * 100).toInt()}",
+ color = Color(0xFFFBBF24)
+ )
+ ModernSliderControl(
+ label = "Contrast",
+ value = contrast,
+ onValueChange = onContrastChange,
+ valueRange = 0f..2f,
+ displayValue = "${(contrast * 100).toInt()}%",
+ color = Color(0xFFEC4899)
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ModernToolButton(tool: EditTool, isSelected: Boolean, onClick: () -> Unit) {
+ val (icon, label) = when (tool) {
+ EditTool.CROP -> Icons.Default.CropFree to "Crop"
+ EditTool.ROTATE -> Icons.Default.RotateRight to "Rotate"
+ EditTool.ADJUST -> Icons.Default.Tune to "Adjust"
+ }
+
+ val backgroundColor by animateColorAsState(
+ targetValue = if (isSelected) Color(0xFF8B5CF6) else Color(0xFF334155),
+ label = "bg_color"
+ )
+
+ val scale by animateFloatAsState(
+ targetValue = if (isSelected) 1.05f else 1f,
+ label = "scale"
+ )
+
+ Surface(
+ onClick = onClick,
+ shape = RoundedCornerShape(20.dp),
+ color = backgroundColor,
+ modifier = Modifier
+ .graphicsLayer(scaleX = scale, scaleY = scale)
+ .padding(4.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(horizontal = 28.dp, vertical = 20.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(
+ icon,
+ null,
+ tint = Color.White,
+ modifier = Modifier.size(32.dp)
+ )
+ Text(
+ label,
+ color = Color.White,
+ fontSize = 13.sp,
+ fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium
+ )
+ }
+ }
+}
+
+@Composable
+fun ModernSliderControl(
+ label: String,
+ value: Float,
+ onValueChange: (Float) -> Unit,
+ valueRange: ClosedFloatingPointRange,
+ displayValue: String,
+ color: Color
+) {
+ var showEditDialog by remember { mutableStateOf(false) }
+
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ label,
+ color = Color.White,
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium
+ )
+ Surface(
+ onClick = { showEditDialog = true },
+ shape = RoundedCornerShape(12.dp),
+ color = color.copy(alpha = 0.2f)
+ ) {
+ Text(
+ displayValue,
+ color = color,
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
+ )
+ }
+ }
+ Slider(
+ value = value,
+ onValueChange = onValueChange,
+ valueRange = valueRange,
+ colors = SliderDefaults.colors(
+ thumbColor = color,
+ activeTrackColor = color,
+ inactiveTrackColor = color.copy(alpha = 0.3f)
+ ),
+ modifier = Modifier.height(48.dp)
+ )
+ }
+
+ if (showEditDialog) {
+ ValueEditDialog(
+ title = label,
+ currentValue = value,
+ valueRange = valueRange,
+ onDismiss = { showEditDialog = false },
+ onConfirm = { newValue ->
+ onValueChange(newValue)
+ showEditDialog = false
+ },
+ color = color
+ )
+ }
+}
+
+@Composable
+fun ValueEditDialog(
+ title: String,
+ currentValue: Float,
+ valueRange: ClosedFloatingPointRange,
+ onDismiss: () -> Unit,
+ onConfirm: (Float) -> Unit,
+ color: Color
+) {
+ var textValue by remember {
+ mutableStateOf(
+ when (title) {
+ "Rotation" -> currentValue.toInt().toString()
+ "Brightness" -> (currentValue * 100).toInt().toString()
+ "Contrast" -> (currentValue * 100).toInt().toString()
+ else -> currentValue.toString()
+ }
+ )
+ }
+
+ Dialog(onDismissRequest = onDismiss) {
+ Card(
+ shape = RoundedCornerShape(24.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ )
+ ) {
+ Column(
+ modifier = Modifier.padding(28.dp),
+ verticalArrangement = Arrangement.spacedBy(20.dp)
+ ) {
+ Text(
+ "Set $title",
+ color = Color.White,
+ fontSize = 20.sp,
+ fontWeight = FontWeight.Bold
+ )
+
+ OutlinedTextField(
+ value = textValue,
+ onValueChange = { textValue = it },
+ label = { Text(title) },
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Number
+ ),
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = Color.Transparent,
+ unfocusedContainerColor = Color.Transparent,
+ focusedTextColor = Color.White,
+ unfocusedTextColor = Color.White,
+ focusedLabelColor = color,
+ unfocusedLabelColor = Color.White.copy(alpha = 0.6f),
+ cursorColor = color,
+ focusedIndicatorColor = color,
+ unfocusedIndicatorColor = Color.White.copy(alpha = 0.3f)
+ ),
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Button(
+ onClick = onDismiss,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color.White.copy(alpha = 0.1f)
+ ),
+ modifier = Modifier.weight(1f)
+ ) {
+ Text("Cancel", color = Color.White)
+ }
+
+ Button(
+ onClick = {
+ val parsedValue = textValue.toFloatOrNull()
+ if (parsedValue != null) {
+ val finalValue = when (title) {
+ "Rotation" -> parsedValue.coerceIn(valueRange)
+ "Brightness" -> (parsedValue / 100f).coerceIn(valueRange)
+ "Contrast" -> (parsedValue / 100f).coerceIn(valueRange)
+ else -> parsedValue.coerceIn(valueRange)
+ }
+ onConfirm(finalValue)
+ }
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = color
+ ),
+ modifier = Modifier.weight(1f)
+ ) {
+ Text("Apply", fontWeight = FontWeight.Bold)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ProcessingChoiceDialog(
+ onDismiss: () -> Unit,
+ onChoice: (ProcessingChoice) -> Unit
+) {
+ Dialog(onDismissRequest = onDismiss) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ shape = RoundedCornerShape(28.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ )
+ ) {
+ Column(
+ modifier = Modifier.padding(28.dp),
+ verticalArrangement = Arrangement.spacedBy(24.dp)
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Icon(
+ Icons.Default.AutoAwesome,
+ null,
+ tint = Color(0xFF8B5CF6),
+ modifier = Modifier.size(48.dp)
+ )
+ Text(
+ "How would you like to proceed?",
+ color = Color.White,
+ fontSize = 20.sp,
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center
+ )
+ }
+
+ ChoiceOptionCard(
+ icon = Icons.Default.Psychology,
+ title = "✨ AI Auto-Fill",
+ description = "Let AI extract all item details automatically (FREE)",
+ color = Color(0xFF8B5CF6),
+ onClick = { onChoice(ProcessingChoice.AI_OCR) }
+ )
+
+ ChoiceOptionCard(
+ icon = Icons.Default.Edit,
+ title = "✍️ Manual Entry",
+ description = "Fill in item details yourself",
+ color = Color(0xFF10B981),
+ onClick = { onChoice(ProcessingChoice.MANUAL) }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun ChoiceOptionCard(
+ icon: androidx.compose.ui.graphics.vector.ImageVector,
+ title: String,
+ description: String,
+ color: Color,
+ onClick: () -> Unit
+) {
+ Card(
+ onClick = onClick,
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(20.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = color.copy(alpha = 0.15f)
+ ),
+ border = BorderStroke(2.dp, color.copy(alpha = 0.3f))
+ ) {
+ Row(
+ modifier = Modifier.padding(20.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Surface(
+ shape = CircleShape,
+ color = color,
+ modifier = Modifier.size(56.dp)
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ Icon(
+ icon,
+ null,
+ tint = Color.White,
+ modifier = Modifier.size(28.dp)
+ )
+ }
+ }
+ Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ Text(
+ title,
+ color = Color.White,
+ fontSize = 17.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ description,
+ color = Color.White.copy(alpha = 0.7f),
+ fontSize = 13.sp
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun LoadingScreen() {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color(0xFF0F172A)),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(
+ color = Color(0xFF8B5CF6),
+ modifier = Modifier.size(64.dp)
+ )
+ }
+}
+
+// Helper Functions
+fun applyImageFilters(bitmap: Bitmap, brightness: Float, contrast: Float): Bitmap {
+ val colorMatrix = ColorMatrix().apply {
+ set(floatArrayOf(
+ contrast, 0f, 0f, 0f, brightness * 255,
+ 0f, contrast, 0f, 0f, brightness * 255,
+ 0f, 0f, contrast, 0f, brightness * 255,
+ 0f, 0f, 0f, 1f, 0f
+ ))
+ }
+ val paint = android.graphics.Paint().apply {
+ colorFilter = ColorMatrixColorFilter(colorMatrix)
+ }
+ val result = Bitmap.createBitmap(bitmap.width, bitmap.height, bitmap.config ?: Bitmap.Config.ARGB_8888)
+ val canvas = android.graphics.Canvas(result)
+ canvas.drawBitmap(bitmap, 0f, 0f, paint)
+ return result
+}
+
+fun applyAllEdits(bitmap: Bitmap, rotation: Float, brightness: Float, contrast: Float): Bitmap {
+ var result = applyImageFilters(bitmap, brightness, contrast)
+
+ if (rotation != 0f) {
+ val matrix = Matrix().apply { postRotate(rotation) }
+ result = Bitmap.createBitmap(result, 0, 0, result.width, result.height, matrix, true)
+ }
+
+ return result
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/ImageProcessingScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/ImageProcessingScreen.kt
new file mode 100644
index 0000000..1f0146d
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/ImageProcessingScreen.kt
@@ -0,0 +1,845 @@
+package com.samuel.inventorymanager.screens
+
+import android.graphics.Bitmap
+import android.graphics.Matrix
+import android.net.Uri
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.core.EaseInOut
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectTransformGestures
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.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.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.material.icons.automirrored.filled.RotateRight
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.CropFree
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Psychology
+import androidx.compose.material.icons.filled.Tune
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+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.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.zIndex
+import com.samuel.inventorymanager.services.AIService
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.withContext
+
+@Composable
+fun ImageProcessingScreen(
+ imageUri: Uri,
+ onImageProcessed: (Uri, AIService.AIAnalysisResult) -> Unit,
+ onCancel: () -> Unit,
+ aiService: AIService
+) {
+ var currentStep by remember { mutableStateOf(ProcessingStep.EDITING) }
+ var editedBitmap by remember { mutableStateOf(null) }
+ var aiResult by remember { mutableStateOf(null) }
+
+ Box(modifier = Modifier.fillMaxSize()) {
+ when (currentStep) {
+ ProcessingStep.EDITING -> {
+ ImageEditorScreen(
+ imageUri = imageUri,
+ onNext = { bitmap ->
+ editedBitmap = bitmap
+ currentStep = ProcessingStep.AI_ANALYZING
+ },
+ onCancel = onCancel
+ )
+ }
+ ProcessingStep.AI_ANALYZING -> {
+ LaunchedEffect(Unit) {
+ delay(500)
+ try {
+ val result = aiService.analyzeItemFromBitmap(editedBitmap!!)
+ aiResult = result
+ currentStep = ProcessingStep.AI_PREVIEW
+ } catch (_: Exception) {
+ onImageProcessed(
+ imageUri,
+ AIService.AIAnalysisResult(
+ itemName = null,
+ confidence = 0.0,
+ modelNumber = null,
+ description = null,
+ estimatedPrice = null,
+ condition = null,
+ sizeCategory = null,
+ dimensions = null,
+ rawText = null
+ )
+ )
+ }
+ }
+ AIAnalyzingScreen()
+ }
+ ProcessingStep.AI_PREVIEW -> {
+ aiResult?.let { result ->
+ editedBitmap?.let { bitmap ->
+ AIPreviewScreen(
+ result = result,
+ bitmap = bitmap,
+ onAccept = { finalResult ->
+ onImageProcessed(imageUri, finalResult)
+ },
+ onEdit = { finalResult ->
+ onImageProcessed(imageUri, finalResult)
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+enum class ProcessingStep {
+ EDITING, AI_ANALYZING, AI_PREVIEW
+}
+
+@Composable
+fun ImageEditorScreen(
+ imageUri: Uri,
+ onNext: (Bitmap) -> Unit,
+ onCancel: () -> Unit
+) {
+ val context = LocalContext.current
+ var bitmap by remember { mutableStateOf(null) }
+ var rotation by remember { mutableFloatStateOf(0f) }
+ var brightness by remember { mutableFloatStateOf(0f) }
+ var contrast by remember { mutableFloatStateOf(1f) }
+ var scale by remember { mutableFloatStateOf(1f) }
+ var currentTool by remember { mutableStateOf(EditorTool.ROTATE) }
+
+ var showRotationDialog by remember { mutableStateOf(false) }
+ var showBrightnessDialog by remember { mutableStateOf(false) }
+ var showContrastDialog by remember { mutableStateOf(false) }
+
+ LaunchedEffect(imageUri) {
+ bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ loadBitmapFromUri(context, imageUri)
+ } else {
+ loadBitmapFromUriLegacy(context, imageUri)
+ }
+ }
+
+ Surface(modifier = Modifier.fillMaxSize(), color = Color(0xFF0A0A0A)) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Column(modifier = Modifier.fillMaxSize()) {
+ EditorTopBar(
+ onCancel = onCancel,
+ onNext = {
+ bitmap?.let { bmp ->
+ val processed = applyEdits(bmp, rotation, brightness, contrast)
+ onNext(processed)
+ }
+ },
+ modifier = Modifier.zIndex(10f)
+ )
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .padding(16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ bitmap?.let { bmp ->
+ EditableImage(
+ bitmap = bmp,
+ rotation = rotation,
+ brightness = brightness,
+ contrast = contrast,
+ scale = scale,
+ onScaleChange = { scale = it },
+ onResetView = {
+ scale = 1f
+ rotation = 0f
+ }
+ )
+ }
+ }
+
+ EditorToolsPanel(
+ currentTool = currentTool,
+ onToolChange = { currentTool = it },
+ rotation = rotation,
+ onRotationChange = { rotation = it },
+ brightness = brightness,
+ onBrightnessChange = { brightness = it },
+ contrast = contrast,
+ onContrastChange = { contrast = it },
+ onRotationClick = { showRotationDialog = true },
+ onBrightnessClick = { showBrightnessDialog = true },
+ onContrastClick = { showContrastDialog = true },
+ modifier = Modifier.zIndex(10f)
+ )
+ }
+
+ if (showRotationDialog) {
+ NumberInputDialog(
+ title = "Rotation",
+ currentValue = rotation,
+ onDismiss = { showRotationDialog = false },
+ onConfirm = {
+ rotation = it.coerceIn(-180f, 180f)
+ showRotationDialog = false
+ },
+ suffix = "°"
+ )
+ }
+
+ if (showBrightnessDialog) {
+ NumberInputDialog(
+ title = "Brightness",
+ currentValue = brightness * 100,
+ onDismiss = { showBrightnessDialog = false },
+ onConfirm = {
+ brightness = (it / 100f).coerceIn(-1f, 1f)
+ showBrightnessDialog = false
+ },
+ suffix = ""
+ )
+ }
+
+ if (showContrastDialog) {
+ NumberInputDialog(
+ title = "Contrast",
+ currentValue = contrast * 100,
+ onDismiss = { showContrastDialog = false },
+ onConfirm = {
+ contrast = (it / 100f).coerceIn(0f, 2f)
+ showContrastDialog = false
+ },
+ suffix = "%"
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun EditorTopBar(onCancel: () -> Unit, onNext: () -> Unit, modifier: Modifier = Modifier) {
+ Surface(
+ modifier = modifier.fillMaxWidth(),
+ color = Color(0xFF1A1A1A),
+ shadowElevation = 4.dp
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ TextButton(onClick = onCancel) {
+ Icon(Icons.Default.Close, null, tint = Color.White)
+ Spacer(Modifier.width(4.dp))
+ Text("Cancel", color = Color.White, fontWeight = FontWeight.Medium)
+ }
+ Text("Edit Image", color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Bold)
+ Button(
+ onClick = onNext,
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF6366F1)),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Text("Next", fontWeight = FontWeight.Bold)
+ Spacer(Modifier.width(4.dp))
+ Icon(Icons.AutoMirrored.Filled.ArrowForward, null)
+ }
+ }
+ }
+}
+
+@Composable
+fun EditableImage(
+ bitmap: Bitmap,
+ rotation: Float,
+ brightness: Float,
+ contrast: Float,
+ scale: Float,
+ onScaleChange: (Float) -> Unit,
+ onResetView: () -> Unit
+) {
+ val filteredBitmap by remember {
+ derivedStateOf {
+ applyFilters(bitmap, brightness, contrast).asImageBitmap()
+ }
+ }
+
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .clip(RoundedCornerShape(16.dp))) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize(0.95f)
+ .align(Alignment.Center)
+ .clip(RoundedCornerShape(12.dp))
+ ) {
+ Image(
+ bitmap = filteredBitmap,
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxSize()
+ .graphicsLayer(rotationZ = rotation, scaleX = scale, scaleY = scale)
+ .pointerInput(Unit) {
+ detectTransformGestures { _, _, zoom, _ ->
+ onScaleChange((scale * zoom).coerceIn(0.5f, 3f))
+ }
+ },
+ contentScale = ContentScale.Fit
+ )
+ }
+
+ IconButton(
+ onClick = onResetView,
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .padding(16.dp)
+ .background(Color(0xFF2A2A2A), RoundedCornerShape(12.dp))
+ ) {
+ Icon(Icons.Default.CropFree, contentDescription = "Reset View", tint = Color.White)
+ }
+ }
+}
+
+enum class EditorTool { ROTATE, ADJUST }
+
+@Composable
+fun EditorToolsPanel(
+ currentTool: EditorTool,
+ onToolChange: (EditorTool) -> Unit,
+ rotation: Float,
+ onRotationChange: (Float) -> Unit,
+ brightness: Float,
+ onBrightnessChange: (Float) -> Unit,
+ contrast: Float,
+ onContrastChange: (Float) -> Unit,
+ onRotationClick: () -> Unit,
+ onBrightnessClick: () -> Unit,
+ onContrastClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ modifier = modifier.fillMaxWidth(),
+ color = Color(0xFF1A1A1A),
+ shadowElevation = 8.dp
+ ) {
+ Column(
+ modifier = Modifier.padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ EditorTool.entries.forEach { tool ->
+ ToolButton(tool = tool, isSelected = currentTool == tool, onClick = { onToolChange(tool) })
+ }
+ }
+
+ AnimatedContent(targetState = currentTool, label = "tool_controls") { tool ->
+ when (tool) {
+ EditorTool.ROTATE -> {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(8.dp))
+ .background(Color(0xFF2A2A2A))
+ .padding(12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text("Rotation:", color = Color.White, fontSize = 14.sp)
+ TextButton(onClick = onRotationClick) {
+ Text(
+ "${rotation.toInt()}°",
+ color = Color(0xFF6366F1),
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+ Slider(
+ value = rotation,
+ onValueChange = onRotationChange,
+ valueRange = -180f..180f,
+ colors = SliderDefaults.colors(
+ thumbColor = Color(0xFF6366F1),
+ activeTrackColor = Color(0xFF6366F1)
+ )
+ )
+ }
+ }
+ EditorTool.ADJUST -> {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Column {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(8.dp))
+ .background(Color(0xFF2A2A2A))
+ .padding(12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text("Brightness:", color = Color.White, fontSize = 14.sp)
+ TextButton(onClick = onBrightnessClick) {
+ Text(
+ "${(brightness * 100).toInt()}",
+ color = Color(0xFFFBBF24),
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+ Slider(
+ value = brightness,
+ onValueChange = onBrightnessChange,
+ valueRange = -1f..1f,
+ colors = SliderDefaults.colors(
+ thumbColor = Color(0xFFFBBF24),
+ activeTrackColor = Color(0xFFFBBF24)
+ )
+ )
+ }
+ Column {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(8.dp))
+ .background(Color(0xFF2A2A2A))
+ .padding(12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text("Contrast:", color = Color.White, fontSize = 14.sp)
+ TextButton(onClick = onContrastClick) {
+ Text(
+ "${(contrast * 100).toInt()}%",
+ color = Color(0xFF8B5CF6),
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+ Slider(
+ value = contrast,
+ onValueChange = onContrastChange,
+ valueRange = 0f..2f,
+ colors = SliderDefaults.colors(
+ thumbColor = Color(0xFF8B5CF6),
+ activeTrackColor = Color(0xFF8B5CF6)
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ToolButton(tool: EditorTool, isSelected: Boolean, onClick: () -> Unit) {
+ val (icon, label) = when (tool) {
+ EditorTool.ROTATE -> Icons.AutoMirrored.Filled.RotateRight to "Rotate"
+ EditorTool.ADJUST -> Icons.Default.Tune to "Adjust"
+ }
+
+ Surface(
+ onClick = onClick,
+ shape = RoundedCornerShape(16.dp),
+ color = if (isSelected) Color(0xFF6366F1) else Color(0xFF2A2A2A),
+ modifier = Modifier.padding(4.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(icon, null, tint = Color.White, modifier = Modifier.size(28.dp))
+ Text(
+ label,
+ color = Color.White,
+ fontSize = 12.sp,
+ fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
+ )
+ }
+ }
+}
+
+@Composable
+fun NumberInputDialog(
+ title: String,
+ currentValue: Float,
+ onDismiss: () -> Unit,
+ onConfirm: (Float) -> Unit,
+ suffix: String
+) {
+ var textValue by remember { mutableStateOf(currentValue.toInt().toString()) }
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text("Set $title", color = Color.White, fontWeight = FontWeight.Bold) },
+ text = {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Text("Enter value:", color = Color.White.copy(alpha = 0.7f), fontSize = 14.sp)
+ OutlinedTextField(
+ value = textValue,
+ onValueChange = { textValue = it },
+ modifier = Modifier.fillMaxWidth(),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = Color(0xFF6366F1),
+ unfocusedBorderColor = Color(0xFF3A3A3A),
+ focusedTextColor = Color.White,
+ unfocusedTextColor = Color.White,
+ cursorColor = Color(0xFF6366F1)
+ ),
+ shape = RoundedCornerShape(12.dp),
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ suffix = { Text(suffix, color = Color.White.copy(alpha = 0.6f)) },
+ singleLine = true
+ )
+ }
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ val value = textValue.toFloatOrNull() ?: currentValue
+ onConfirm(value)
+ },
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF6366F1)),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Text("Apply", fontWeight = FontWeight.Bold)
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text("Cancel", color = Color.White.copy(alpha = 0.7f))
+ }
+ },
+ containerColor = Color(0xFF1A1A1A),
+ shape = RoundedCornerShape(20.dp)
+ )
+}
+
+@Composable
+fun AIAnalyzingScreen() {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color(0xFF0A0A0A)),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(24.dp)
+ ) {
+ val infiniteTransition = rememberInfiniteTransition(label = "ai_pulse")
+ val scale by infiniteTransition.animateFloat(
+ initialValue = 1f,
+ targetValue = 1.2f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1000, easing = EaseInOut),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "scale"
+ )
+
+ Icon(
+ Icons.Default.Psychology,
+ contentDescription = null,
+ modifier = Modifier
+ .size(80.dp)
+ .graphicsLayer(scaleX = scale, scaleY = scale),
+ tint = Color(0xFF6366F1)
+ )
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text("AI Analyzing Image...", color = Color.White, fontSize = 24.sp, fontWeight = FontWeight.Bold)
+ Text(
+ "Extracting item details automatically",
+ color = Color.White.copy(alpha = 0.6f),
+ fontSize = 14.sp
+ )
+ }
+
+ CircularProgressIndicator(color = Color(0xFF6366F1), modifier = Modifier.size(48.dp))
+ }
+ }
+}
+
+@Composable
+fun AIPreviewScreen(
+ result: AIService.AIAnalysisResult,
+ bitmap: Bitmap,
+ onAccept: (AIService.AIAnalysisResult) -> Unit,
+ onEdit: (AIService.AIAnalysisResult) -> Unit
+) {
+ var editedResult by remember { mutableStateOf(result) }
+ var isEditing by remember { mutableStateOf(false) }
+
+ Surface(modifier = Modifier.fillMaxSize(), color = Color(0xFF0F0F0F)) {
+ Column(modifier = Modifier.fillMaxSize()) {
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp)) {
+ Image(
+ bitmap = bitmap.asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop,
+ alpha = 0.4f
+ )
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ Brush.verticalGradient(colors = listOf(Color.Transparent, Color(0xFF0F0F0F)))
+ )
+ )
+ }
+
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState())
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text("✨ AI Detected (AI is not perfect)", color = Color(0xFF6366F1), fontSize = 22.sp, fontWeight = FontWeight.Bold)
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(20.dp),
+ colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
+ ) {
+ Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text("Detected Information", color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Bold)
+ IconButton(onClick = { isEditing = !isEditing }) {
+ Icon(
+ if (isEditing) Icons.Default.Check else Icons.Default.Edit,
+ null,
+ tint = Color(0xFF6366F1)
+ )
+ }
+ }
+
+ AIField("Item Name", editedResult.itemName, isEditing) {
+ editedResult = editedResult.copy(itemName = it)
+ }
+ AIField("Model", editedResult.modelNumber, isEditing) {
+ editedResult = editedResult.copy(modelNumber = it)
+ }
+ AIField("Description", editedResult.description, isEditing, maxLines = 3) {
+ editedResult = editedResult.copy(description = it)
+ }
+ AIField("Condition", editedResult.condition, isEditing) {
+ editedResult = editedResult.copy(condition = it)
+ }
+ AIField("Size Category", editedResult.sizeCategory, isEditing) {
+ editedResult = editedResult.copy(sizeCategory = it)
+ }
+ editedResult.estimatedPrice?.let { price ->
+ AIField("Est. Price", "$${price}", isEditing) {
+ editedResult = editedResult.copy(estimatedPrice = it.replace("$", "").toDoubleOrNull())
+ }
+ }
+ }
+ }
+ }
+
+ Surface(modifier = Modifier.fillMaxWidth(), color = Color(0xFF1A1A1A), shadowElevation = 8.dp) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Button(
+ onClick = { onEdit(editedResult) },
+ modifier = Modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2A2A2A)),
+ shape = RoundedCornerShape(14.dp),
+ contentPadding = PaddingValues(18.dp)
+ ) {
+ Icon(Icons.Default.Edit, null)
+ Spacer(Modifier.width(8.dp))
+ Text("Edit More", fontSize = 15.sp, fontWeight = FontWeight.Bold)
+ }
+ Button(
+ onClick = { onAccept(editedResult) },
+ modifier = Modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF10B981)),
+ shape = RoundedCornerShape(14.dp),
+ contentPadding = PaddingValues(18.dp)
+ ) {
+ Icon(Icons.Default.Check, null)
+ Spacer(Modifier.width(8.dp))
+ Text("Accept", fontSize = 15.sp, fontWeight = FontWeight.Bold)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun AIField(
+ label: String,
+ value: String?,
+ isEditing: Boolean,
+ maxLines: Int = 1,
+ onValueChange: (String) -> Unit
+) {
+ Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
+ Text(label, color = Color.White.copy(alpha = 0.6f), fontSize = 12.sp, fontWeight = FontWeight.Medium)
+ if (isEditing) {
+ OutlinedTextField(
+ value = value ?: "",
+ onValueChange = onValueChange,
+ modifier = Modifier.fillMaxWidth(),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = Color(0xFF6366F1),
+ unfocusedBorderColor = Color(0xFF2A2A2A),
+ focusedTextColor = Color.White,
+ unfocusedTextColor = Color.White
+ ),
+ shape = RoundedCornerShape(12.dp),
+ maxLines = maxLines
+ )
+ } else {
+ Text(
+ value ?: "Not detected",
+ color = if (value != null) Color.White else Color.White.copy(alpha = 0.4f),
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium
+ )
+ }
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.P)
+suspend fun loadBitmapFromUri(context: android.content.Context, uri: Uri): Bitmap? {
+ return try {
+ withContext(kotlinx.coroutines.Dispatchers.IO) {
+ val source = android.graphics.ImageDecoder.createSource(context.contentResolver, uri)
+ android.graphics.ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
+ decoder.isMutableRequired = true
+ }
+ }
+ } catch (_: Exception) { // Changed from (e: Exception) to (_: Exception)
+ null
+ }
+}
+@Suppress("DEPRECATION")
+suspend fun loadBitmapFromUriLegacy(context: android.content.Context, uri: Uri): Bitmap? {
+ return try {
+ withContext(kotlinx.coroutines.Dispatchers.IO) {
+ android.provider.MediaStore.Images.Media.getBitmap(context.contentResolver, uri)
+ }
+ } catch (_: Exception) {
+ null
+ }
+}
+fun applyFilters(bitmap: Bitmap, brightness: Float, contrast: Float): Bitmap {
+ val colorMatrix = android.graphics.ColorMatrix().apply {
+ set(floatArrayOf(
+ contrast, 0f, 0f, 0f, brightness * 255,
+ 0f, contrast, 0f, 0f, brightness * 255,
+ 0f, 0f, contrast, 0f, brightness * 255,
+ 0f, 0f, 0f, 1f, 0f
+ ))
+ }
+ val paint = android.graphics.Paint().apply {
+ colorFilter = android.graphics.ColorMatrixColorFilter(colorMatrix)
+ }
+ val safeConfig = bitmap.config ?: Bitmap.Config.ARGB_8888
+ // Using androidx.core.graphics.createBitmap extension function
+ val result = androidx.core.graphics.createBitmap(bitmap.width, bitmap.height, safeConfig)
+ val canvas = android.graphics.Canvas(result)
+ canvas.drawBitmap(bitmap, 0f, 0f, paint)
+ return result
+}
+
+fun applyEdits(bitmap: Bitmap, rotation: Float, brightness: Float, contrast: Float): Bitmap {
+ var result = applyFilters(bitmap, brightness, contrast)
+
+ if (rotation != 0f) {
+ val matrix = Matrix().apply { postRotate(rotation) }
+ result = Bitmap.createBitmap(result, 0, 0, result.width, result.height, matrix, true)
+ }
+
+ return result
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/ImagesScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/ImagesScreen.kt
index a8c4af9..b10425c 100644
--- a/app/src/main/java/com/samuel/inventorymanager/screens/ImagesScreen.kt
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/ImagesScreen.kt
@@ -1,6 +1,5 @@
package com.samuel.inventorymanager.screens
-import android.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -8,130 +7,602 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
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.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Sort
+import androidx.compose.material.icons.filled.ArrowDownward
+import androidx.compose.material.icons.filled.ArrowUpward
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.DeleteSweep
+import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.ImageSearch
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.ZoomIn
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import androidx.core.net.toUri
import coil.compose.rememberAsyncImagePainter
+import com.google.gson.Gson
+import java.io.File
+
+// ========================================================================================
+// DELETED IMAGE TRACKING
+// ========================================================================================
+
+data class DeletedImageEntry(
+ val imageUrl: String,
+ val itemId: String,
+ val deletedAt: Long = System.currentTimeMillis()
+)
+
+data class DeletedImagesData(
+ val deletedImages: List = emptyList()
+)
-// We create a simple data class to make the logic cleaner.
-// It bundles an image with the item it belongs to.
data class ImageEntry(
val item: Item,
- val imageUrl: String
+ val imageUrl: String,
+ val isDeleted: Boolean = false
)
+enum class ImageSortOption {
+ NAME_ASC,
+ NAME_DESC,
+ DATE_NEWEST,
+ DATE_OLDEST,
+ CATEGORY_ASC,
+ CATEGORY_DESC
+}
+
+private fun saveDeletedImages(context: android.content.Context, deletedImages: List) {
+ try {
+ context.openFileOutput("deleted_images.json", android.content.Context.MODE_PRIVATE).use {
+ it.write(Gson().toJson(DeletedImagesData(deletedImages)).toByteArray())
+ }
+ } catch (_: Exception) {
+ // Handle silently
+ }
+}
+
+private fun loadDeletedImages(context: android.content.Context): List {
+ val file = File(context.filesDir, "deleted_images.json")
+ return if (file.exists()) {
+ try {
+ val data = Gson().fromJson(file.readText(), DeletedImagesData::class.java)
+ // Remove images that were deleted more than 2 days ago
+ val twoDaysMs = 2 * 24 * 60 * 60 * 1000L
+ val now = System.currentTimeMillis()
+ data.deletedImages.filter { now - it.deletedAt < twoDaysMs }
+ } catch (_: Exception) {
+ emptyList()
+ }
+ } else {
+ emptyList()
+ }
+}
+
+// ========================================================================================
+// MAIN IMAGES SCREEN
+// ========================================================================================
+
@Composable
fun ImagesScreen(
items: List
- ,
- onItemClick: (Item) -> Unit // This function will trigger the navigation
+ onItemClick: (Item) -> Unit
) {
- // We first gather all images from all items into a single, flat list.
- val allImages = items.flatMap { item ->
- item.images.map { imageUrl ->
- ImageEntry(item = item, imageUrl = imageUrl)
+ val context = LocalContext.current
+ var searchQuery by remember { mutableStateOf("") }
+ var deletedImages by remember { mutableStateOf(loadDeletedImages(context)) }
+ var selectedImageEntry by remember { mutableStateOf(null) }
+ var showZoomDialog by remember { mutableStateOf(false) }
+ var sortOption by remember { mutableStateOf(ImageSortOption.NAME_ASC) }
+ var showSortMenu by remember { mutableStateOf(false) }
+
+ // Filter items to only include those with images
+ val itemsWithImages = remember(items) {
+ items.filter { it.images.isNotEmpty() }
+ }
+
+ // Gather all images and mark deleted ones
+ val allImages = remember(itemsWithImages, deletedImages) {
+ itemsWithImages.flatMap { item ->
+ item.images.map { imageUrl ->
+ val isDeleted = deletedImages.any { it.imageUrl == imageUrl && it.itemId == item.id }
+ ImageEntry(item = item, imageUrl = imageUrl, isDeleted = isDeleted)
+ }
}
}
- if (allImages.isEmpty()) {
- // Show a helpful message if no images have been added yet.
- EmptyState()
- } else {
- // Display the images in a responsive, vertical grid.
- LazyVerticalGrid(
- columns = GridCells.Adaptive(minSize = 120.dp), // This makes the grid look good on any screen size!
- modifier = Modifier.fillMaxSize(),
- contentPadding = PaddingValues(16.dp),
- horizontalArrangement = Arrangement.spacedBy(16.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp)
+ // Filter by search query
+ val filteredImages = remember(allImages, searchQuery) {
+ allImages.filter { entry ->
+ searchQuery.isBlank() ||
+ entry.item.name.contains(searchQuery, ignoreCase = true) ||
+ entry.item.modelNumber?.contains(searchQuery, ignoreCase = true) == true
+ }
+ }
+
+ // Sort images
+ val sortedImages = remember(filteredImages, sortOption) {
+ when (sortOption) {
+ ImageSortOption.NAME_ASC -> filteredImages.sortedBy { it.item.name.lowercase() }
+ ImageSortOption.NAME_DESC -> filteredImages.sortedByDescending { it.item.name.lowercase() }
+ ImageSortOption.DATE_NEWEST -> {
+ // Sort by item index in reverse (newest items first)
+ val itemIndices = items.mapIndexed { index, item -> item.id to index }.toMap()
+ filteredImages.sortedByDescending { itemIndices[it.item.id] ?: 0 }
+ }
+ ImageSortOption.DATE_OLDEST -> {
+ // Sort by item index (oldest items first)
+ val itemIndices = items.mapIndexed { index, item -> item.id to index }.toMap()
+ filteredImages.sortedBy { itemIndices[it.item.id] ?: 0 }
+ }
+ ImageSortOption.CATEGORY_ASC -> filteredImages.sortedBy { it.item.sizeCategory.lowercase() }
+ ImageSortOption.CATEGORY_DESC -> filteredImages.sortedByDescending { it.item.sizeCategory.lowercase() }
+ }
+ }
+
+ Column(modifier = Modifier.fillMaxSize()) {
+ // Header with Search and Sort
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ shape = RoundedCornerShape(0.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
- items(allImages) { imageEntry ->
- ImageCard(
- imageEntry = imageEntry,
- onClick = {
- // When an image is clicked, we call the navigation function
- // with the item that the image belongs to.
- onItemClick(imageEntry.item)
+ Column(modifier = Modifier.padding(16.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ OutlinedTextField(
+ value = searchQuery,
+ onValueChange = { searchQuery = it },
+ modifier = Modifier
+ .weight(1f)
+ .height(48.dp),
+ placeholder = { Text("Search items...") },
+ leadingIcon = {
+ Icon(Icons.Default.Search, contentDescription = "Search")
+ },
+ trailingIcon = {
+ if (searchQuery.isNotEmpty()) {
+ IconButton(onClick = { searchQuery = "" }) {
+ Icon(Icons.Default.Clear, contentDescription = "Clear")
+ }
+ }
+ },
+ singleLine = true,
+ shape = RoundedCornerShape(12.dp)
+ )
+
+ // Sort button
+ Box {
+ Button(
+ onClick = { showSortMenu = true },
+ modifier = Modifier.height(48.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondaryContainer
+ )
+ ) {
+ Icon(
+ Icons.AutoMirrored.Filled.Sort,
+ contentDescription = "Sort",
+ modifier = Modifier.size(18.dp)
+ )
+ }
+
+ DropdownMenu(
+ expanded = showSortMenu,
+ onDismissRequest = { showSortMenu = false }
+ ) {
+ DropdownMenuItem(
+ text = {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text("Name")
+ Spacer(Modifier.width(8.dp))
+ Icon(Icons.Default.ArrowUpward, null, modifier = Modifier.size(16.dp))
+ }
+ },
+ onClick = {
+ sortOption = ImageSortOption.NAME_ASC
+ showSortMenu = false
+ }
+ )
+ DropdownMenuItem(
+ text = {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text("Name")
+ Spacer(Modifier.width(8.dp))
+ Icon(Icons.Default.ArrowDownward, null, modifier = Modifier.size(16.dp))
+ }
+ },
+ onClick = {
+ sortOption = ImageSortOption.NAME_DESC
+ showSortMenu = false
+ }
+ )
+ DropdownMenuItem(
+ text = { Text("Newest First") },
+ onClick = {
+ sortOption = ImageSortOption.DATE_NEWEST
+ showSortMenu = false
+ }
+ )
+ DropdownMenuItem(
+ text = { Text("Oldest First") },
+ onClick = {
+ sortOption = ImageSortOption.DATE_OLDEST
+ showSortMenu = false
+ }
+ )
+ DropdownMenuItem(
+ text = {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text("Size Category")
+ Spacer(Modifier.width(8.dp))
+ Icon(Icons.Default.ArrowUpward, null, modifier = Modifier.size(16.dp))
+ }
+ },
+ onClick = {
+ sortOption = ImageSortOption.CATEGORY_ASC
+ showSortMenu = false
+ }
+ )
+ DropdownMenuItem(
+ text = {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text("Size Category")
+ Spacer(Modifier.width(8.dp))
+ Icon(Icons.Default.ArrowDownward, null, modifier = Modifier.size(16.dp))
+ }
+ },
+ onClick = {
+ sortOption = ImageSortOption.CATEGORY_DESC
+ showSortMenu = false
+ }
+ )
+ }
}
- )
+
+ // Clean deleted images button
+ Button(
+ onClick = {
+ deletedImages = emptyList()
+ saveDeletedImages(context, emptyList())
+ },
+ modifier = Modifier.height(48.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer
+ ),
+ enabled = deletedImages.isNotEmpty()
+ ) {
+ Icon(Icons.Default.DeleteSweep, contentDescription = null, modifier = Modifier.size(18.dp))
+ }
+ }
+ }
+ }
+
+ // Results
+ if (sortedImages.isEmpty()) {
+ EmptyState()
+ } else {
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(minSize = 120.dp),
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ items(sortedImages) { imageEntry ->
+ ImageCard(
+ imageEntry = imageEntry,
+ onImageClick = {
+ selectedImageEntry = imageEntry
+ showZoomDialog = true
+ },
+ onEditClick = {
+ onItemClick(imageEntry.item)
+ },
+ onDeleteClick = {
+ // Mark image as deleted
+ val newDeletedEntry = DeletedImageEntry(
+ imageUrl = imageEntry.imageUrl,
+ itemId = imageEntry.item.id,
+ deletedAt = System.currentTimeMillis()
+ )
+ deletedImages = deletedImages + newDeletedEntry
+ saveDeletedImages(context, deletedImages)
+ }
+ )
+ }
}
}
}
-}
+ // Zoom Dialog
+ if (showZoomDialog && selectedImageEntry != null) {
+ ZoomImageDialog(
+ imageEntry = selectedImageEntry!!,
+ onDismiss = { showZoomDialog = false },
+ onEditClick = {
+ onItemClick(selectedImageEntry!!.item)
+ showZoomDialog = false
+ }
+ )
+ }
+}
-// ======================================================================
-// HELPER COMPOSABLES
-// These are the building blocks that make the screen look polished. ✨
-// ======================================================================
+// ========================================================================================
+// HELPER COMPOSABLES
+// ========================================================================================
@Composable
private fun ImageCard(
imageEntry: ImageEntry,
- onClick: () -> Unit
+ onImageClick: () -> Unit,
+ onEditClick: () -> Unit,
+ onDeleteClick: () -> Unit
) {
- Card(
- modifier = Modifier
- .aspectRatio(1f) // This makes the card a perfect square
- .clickable(onClick = onClick),
- elevation = CardDefaults.cardElevation(4.dp),
- shape = MaterialTheme.shapes.medium
- ) {
- Box(modifier = Modifier.fillMaxSize()) {
- // The Image itself, which fills the entire card
- Image(
- painter = rememberAsyncImagePainter(model = Uri.parse(imageEntry.imageUrl)),
- contentDescription = imageEntry.item.name,
- modifier = Modifier.fillMaxSize(),
- contentScale = ContentScale.Crop // This ensures the image fills the square without stretching
- )
+ Box(modifier = Modifier.aspectRatio(1f)) {
+ Card(
+ modifier = Modifier
+ .fillMaxSize()
+ .clickable(onClick = onImageClick),
+ elevation = CardDefaults.cardElevation(4.dp),
+ shape = MaterialTheme.shapes.medium
+ ) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ // Image
+ Image(
+ painter = rememberAsyncImagePainter(model = imageEntry.imageUrl.toUri()),
+ contentDescription = imageEntry.item.name,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
- // A semi-transparent overlay at the bottom to make the text readable
- Box(
- modifier = Modifier.run {
- fillMaxWidth()
- .fillMaxHeight(0.4f)
- .background(
- brush = androidx.compose.ui.graphics.Brush.verticalGradient(
- colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.8f))
- )
- )
- .align(Alignment.BottomCenter)
+ // Deleted overlay
+ if (imageEntry.isDeleted) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Black.copy(alpha = 0.6f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ Icons.Default.Close,
+ contentDescription = null,
+ tint = Color.Red,
+ modifier = Modifier.size(40.dp)
+ )
+ Spacer(Modifier.height(8.dp))
+ Text(
+ "Deleted",
+ color = Color.Red,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.labelSmall
+ )
+ }
+ }
}
- )
- // The name of the item the image belongs to
- Text(
- text = imageEntry.item.name,
+ // Gradient overlay for text
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxHeight(0.4f)
+ .background(
+ brush = androidx.compose.ui.graphics.Brush.verticalGradient(
+ colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.8f))
+ )
+ )
+ .align(Alignment.BottomCenter)
+ )
+
+ // Item name
+ Text(
+ text = imageEntry.item.name,
+ modifier = Modifier
+ .align(Alignment.BottomStart)
+ .padding(8.dp),
+ color = Color.White,
+ fontWeight = FontWeight.Bold,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.labelSmall
+ )
+ }
+ }
+
+ // Action buttons (top right)
+ if (!imageEntry.isDeleted) {
+ Row(
modifier = Modifier
- .align(Alignment.BottomStart)
+ .align(Alignment.TopEnd)
.padding(8.dp),
- color = Color.White,
- fontWeight = FontWeight.Bold,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis
+ horizontalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ // Zoom button
+ IconButton(
+ onClick = onImageClick,
+ modifier = Modifier
+ .size(32.dp)
+ .background(
+ Color.Black.copy(alpha = 0.6f),
+ RoundedCornerShape(6.dp)
+ )
+ ) {
+ Icon(
+ Icons.Default.ZoomIn,
+ contentDescription = "Zoom",
+ tint = Color.White,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+
+ // Edit button
+ IconButton(
+ onClick = onEditClick,
+ modifier = Modifier
+ .size(32.dp)
+ .background(
+ Color.Black.copy(alpha = 0.6f),
+ RoundedCornerShape(6.dp)
+ )
+ ) {
+ Icon(
+ Icons.Default.Edit,
+ contentDescription = "Edit",
+ tint = Color.White,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+ }
+ } else {
+ // Show delete button for deleted images (to allow permanent removal if needed)
+ @Suppress("UNUSED_EXPRESSION")
+ onDeleteClick // Keep parameter to avoid warning
+ }
+ }
+}
+
+@Composable
+private fun ZoomImageDialog(
+ imageEntry: ImageEntry,
+ onDismiss: () -> Unit,
+ onEditClick: () -> Unit
+) {
+ Dialog(
+ onDismissRequest = onDismiss,
+ properties = DialogProperties(usePlatformDefaultWidth = false)
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Black.copy(alpha = 0.95f))
+ ) {
+ // Close button (top left)
+ IconButton(
+ onClick = onDismiss,
+ modifier = Modifier
+ .align(Alignment.TopStart)
+ .padding(16.dp)
+ ) {
+ Icon(
+ Icons.Default.Close,
+ contentDescription = "Close",
+ tint = Color.White,
+ modifier = Modifier.size(28.dp)
+ )
+ }
+
+ // Edit button (top right)
+ IconButton(
+ onClick = onEditClick,
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .padding(16.dp)
+ ) {
+ Icon(
+ Icons.Default.Edit,
+ contentDescription = "Edit Item",
+ tint = Color.White,
+ modifier = Modifier.size(28.dp)
+ )
+ }
+
+ // Main image
+ Image(
+ painter = rememberAsyncImagePainter(model = imageEntry.imageUrl.toUri()),
+ contentDescription = imageEntry.item.name,
+ modifier = Modifier
+ .fillMaxSize()
+ .clickable(onClick = onDismiss),
+ contentScale = ContentScale.Fit
)
+
+ // Item info at bottom
+ Column(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .fillMaxWidth()
+ .background(
+ Color.Black.copy(alpha = 0.7f),
+ RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
+ )
+ .padding(16.dp)
+ ) {
+ Text(
+ text = imageEntry.item.name,
+ color = Color.White,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.titleMedium
+ )
+ if (imageEntry.item.modelNumber != null) {
+ Spacer(Modifier.height(4.dp))
+ Text(
+ text = "Model: ${imageEntry.item.modelNumber}",
+ color = Color.White.copy(alpha = 0.8f),
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ if (imageEntry.item.description != null) {
+ Spacer(Modifier.height(4.dp))
+ Text(
+ text = imageEntry.item.description,
+ color = Color.White.copy(alpha = 0.8f),
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
}
}
}
@@ -139,7 +610,9 @@ private fun ImageCard(
@Composable
private fun EmptyState() {
Box(
- modifier = Modifier.fillMaxSize().padding(16.dp),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
contentAlignment = Alignment.Center
) {
Column(
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/LocationsScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/LocationsScreen.kt
index 0619484..0d5a8f8 100644
--- a/app/src/main/java/com/samuel/inventorymanager/screens/LocationsScreen.kt
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/LocationsScreen.kt
@@ -1,7 +1,16 @@
package com.samuel.inventorymanager.screens
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -10,6 +19,8 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -17,20 +28,36 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ViewList
import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.AttachMoney
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
+import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.filled.HomeWork
import androidx.compose.material.icons.filled.Inventory
import androidx.compose.material.icons.filled.Inventory2
import androidx.compose.material.icons.filled.Kitchen
import androidx.compose.material.icons.filled.LocationCity
import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.Preview
+import androidx.compose.material.icons.filled.ViewAgenda
+import androidx.compose.material.icons.filled.ViewModule
+import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
@@ -39,9 +66,12 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -50,10 +80,19 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import androidx.core.net.toUri
+import coil.compose.rememberAsyncImagePainter
import java.util.Locale
@Composable
@@ -66,193 +105,2842 @@ fun LocationsScreen(
onAddBox: (shelfId: String) -> Unit,
onRenameLocation: (id: String, oldName: String, type: String) -> Unit,
onDeleteLocation: (id: String, name: String, type: String) -> Unit
+) {
+ var isPreviewMode by remember { mutableStateOf(false) }
+
+ if (isPreviewMode) {
+ PreviewMode(
+ garages = garages,
+ items = items,
+ onBackToEdit = { isPreviewMode = false }
+ )
+ } else {
+ EditMode(
+ garages = garages,
+ items = items,
+ onAddGarage = onAddGarage,
+ onAddCabinet = onAddCabinet,
+ onAddShelf = onAddShelf,
+ onAddBox = onAddBox,
+ onRenameLocation = onRenameLocation,
+ onDeleteLocation = onDeleteLocation,
+ onSwitchToPreview = { isPreviewMode = true }
+ )
+ }
+}
+
+// ========================================================================================
+// EDIT MODE
+// ========================================================================================
+
+@Composable
+private fun EditMode(
+ garages: List,
+ items: List
- ,
+ onAddGarage: () -> Unit,
+ onAddCabinet: (garageId: String) -> Unit,
+ onAddShelf: (cabinetId: String) -> Unit,
+ onAddBox: (shelfId: String) -> Unit,
+ onRenameLocation: (id: String, oldName: String, type: String) -> Unit,
+ onDeleteLocation: (id: String, name: String, type: String) -> Unit,
+ onSwitchToPreview: () -> Unit
+) {
+ Scaffold(
+ topBar = {
+ Surface(
+ shadowElevation = 3.dp,
+ tonalElevation = 2.dp
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp, vertical = 16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column {
+ Text(
+ "Edit Locations",
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "${garages.size} garages • ${items.size} items",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ IconButton(
+ onClick = onSwitchToPreview,
+ modifier = Modifier
+ .background(
+ MaterialTheme.colorScheme.primaryContainer,
+ RoundedCornerShape(12.dp)
+ )
+ ) {
+ Icon(
+ Icons.Default.Preview,
+ "Preview Mode",
+ tint = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+ }
+ }
+ },
+ floatingActionButton = {
+ FloatingActionButton(
+ onClick = onAddGarage,
+ containerColor = MaterialTheme.colorScheme.primary,
+ shape = RoundedCornerShape(16.dp),
+ modifier = Modifier.shadow(8.dp, RoundedCornerShape(16.dp))
+ ) {
+ Icon(
+ Icons.Default.Add,
+ contentDescription = "Add Garage",
+ tint = MaterialTheme.colorScheme.onPrimary
+ )
+ }
+ }
+ ) { paddingValues ->
+ if (garages.isEmpty()) {
+ EmptyState(onAddGarage, modifier = Modifier.padding(paddingValues))
+ } else {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ items(garages, key = { it.id }) { garage ->
+ val itemsInGarage = items.filter { it.garageId == garage.id }
+ GarageSection(
+ garage = garage,
+ itemsInGarage = itemsInGarage,
+ onAddCabinet = { onAddCabinet(garage.id) },
+ onAddShelf = onAddShelf,
+ onAddBox = onAddBox,
+ onRenameLocation = onRenameLocation,
+ onDeleteLocation = onDeleteLocation
+ )
+ }
+ }
+ }
+ }
+}
+
+// ========================================================================================
+// MULTI-ADD DIALOG
+// ========================================================================================
+
+@Composable
+private fun MultiAddDialog(
+ title: String,
+ onDismiss: () -> Unit,
+ onConfirm: (count: Int) -> Unit
+) {
+ var count by remember { mutableStateOf("1") }
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ icon = {
+ Box(
+ modifier = Modifier
+ .size(56.dp)
+ .background(
+ MaterialTheme.colorScheme.primaryContainer,
+ CircleShape
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.Default.Add,
+ null,
+ tint = MaterialTheme.colorScheme.onPrimaryContainer,
+ modifier = Modifier.size(32.dp)
+ )
+ }
+ },
+ title = {
+ Text(
+ title,
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ text = {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ "How many would you like to add?",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ OutlinedTextField(
+ value = count,
+ onValueChange = {
+ if (it.isEmpty() || it.toIntOrNull() != null) {
+ count = it
+ }
+ },
+ label = { Text("Quantity") },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ singleLine = true,
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp)
+ )
+
+ // Quick select buttons
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ listOf(1, 3, 5, 10).forEach { num ->
+ OutlinedButton(
+ onClick = { count = num.toString() },
+ modifier = Modifier.weight(1f),
+ colors = ButtonDefaults.outlinedButtonColors(
+ containerColor = if (count == num.toString())
+ MaterialTheme.colorScheme.primaryContainer
+ else
+ Color.Transparent
+ )
+ ) {
+ Text(num.toString())
+ }
+ }
+ }
+ }
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ val num = count.toIntOrNull()
+ if (num != null && num > 0) {
+ onConfirm(num)
+ onDismiss()
+ }
+ },
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Text("Add")
+ }
+ },
+ dismissButton = {
+ TextButton(
+ onClick = onDismiss,
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Text("Cancel")
+ }
+ },
+ shape = RoundedCornerShape(24.dp)
+ )
+}
+
+// ========================================================================================
+// PREVIEW MODE
+// ========================================================================================
+
+private enum class PreviewStyle {
+ DRILL_DOWN, COLLAPSIBLE
+}
+
+private enum class ItemViewMode {
+ GRID, LIST
+}
+
+@Composable
+private fun PreviewMode(
+ garages: List,
+ items: List
- ,
+ onBackToEdit: () -> Unit
+) {
+ var previewStyle by remember { mutableStateOf(PreviewStyle.DRILL_DOWN) }
+
+ when (previewStyle) {
+ PreviewStyle.DRILL_DOWN -> {
+ DrillDownNavigationHost(
+ garages = garages,
+ items = items,
+ onBackToEdit = onBackToEdit,
+ onSwitchStyle = { previewStyle = PreviewStyle.COLLAPSIBLE }
+ )
+ }
+ PreviewStyle.COLLAPSIBLE -> {
+ CollapsibleListPreview(
+ garages = garages,
+ items = items,
+ onBackToEdit = onBackToEdit,
+ onSwitchStyle = { previewStyle = PreviewStyle.DRILL_DOWN }
+ )
+ }
+ }
+}
+
+// ========================================================================================
+// DRILL-DOWN PREVIEW
+// ========================================================================================
+
+@Composable
+private fun DrillDownNavigationHost(
+ garages: List,
+ items: List
- ,
+ onBackToEdit: () -> Unit,
+ onSwitchStyle: () -> Unit
+) {
+ var selectedGarageId by remember { mutableStateOf(null) }
+ var selectedCabinetId by remember { mutableStateOf(null) }
+ var selectedShelfId by remember { mutableStateOf(null) }
+ var selectedBoxId by remember { mutableStateOf(null) }
+ var boxLayout by remember { mutableStateOf(BoxLayout.SCATTERED) }
+ var showAllItemsInGarage by remember { mutableStateOf(false) }
+ var showAllItemsInCabinet by remember { mutableStateOf(false) }
+ var showAllItemsInShelf by remember { mutableStateOf(false) }
+
+ when {
+ selectedBoxId != null -> {
+ val garage = garages.find { it.id == selectedGarageId }
+ val cabinet = garage?.cabinets?.find { it.id == selectedCabinetId }
+ val shelf = cabinet?.shelves?.find { it.id == selectedShelfId }
+ val box = shelf?.boxes?.find { it.id == selectedBoxId }
+ val itemsInBox = items.filter { it.boxId == selectedBoxId }
+
+ if (box != null) {
+ BoxPreviewScreen(
+ box = box,
+ itemsInBox = itemsInBox,
+ boxLayout = boxLayout,
+ onLayoutChange = { boxLayout = it },
+ onBack = {
+ selectedBoxId = null
+ showAllItemsInShelf = false
+ }
+ )
+ }
+ }
+ showAllItemsInShelf && selectedShelfId != null -> {
+ val garage = garages.find { it.id == selectedGarageId }
+ val cabinet = garage?.cabinets?.find { it.id == selectedCabinetId }
+ val shelf = cabinet?.shelves?.find { it.id == selectedShelfId }
+ val itemsInShelf = items.filter { it.shelfId == selectedShelfId }
+
+ if (shelf != null) {
+ AllItemsInLocationScreen(
+ locationName = shelf.name,
+ locationType = "Shelf",
+ items = itemsInShelf,
+ onBack = { showAllItemsInShelf = false }
+ )
+ }
+ }
+ selectedShelfId != null -> {
+ val garage = garages.find { it.id == selectedGarageId }
+ val cabinet = garage?.cabinets?.find { it.id == selectedCabinetId }
+ val shelf = cabinet?.shelves?.find { it.id == selectedShelfId }
+ val itemsInShelf = items.filter { it.shelfId == selectedShelfId }
+ val boxesInShelf = shelf?.boxes ?: emptyList()
+
+ if (shelf != null) {
+ ShelfPreviewScreen(
+ shelf = shelf,
+ itemsInShelf = itemsInShelf,
+ boxesInShelf = boxesInShelf,
+ onBoxClick = { boxId -> selectedBoxId = boxId },
+ onShowAllItems = { showAllItemsInShelf = true },
+ onBack = {
+ selectedShelfId = null
+ showAllItemsInCabinet = false
+ }
+ )
+ }
+ }
+ showAllItemsInCabinet && selectedCabinetId != null -> {
+ val garage = garages.find { it.id == selectedGarageId }
+ val cabinet = garage?.cabinets?.find { it.id == selectedCabinetId }
+ val itemsInCabinet = items.filter { it.cabinetId == selectedCabinetId }
+
+ if (cabinet != null) {
+ AllItemsInLocationScreen(
+ locationName = cabinet.name,
+ locationType = "Cabinet",
+ items = itemsInCabinet,
+ onBack = { showAllItemsInCabinet = false }
+ )
+ }
+ }
+ selectedCabinetId != null -> {
+ val garage = garages.find { it.id == selectedGarageId }
+ val cabinet = garage?.cabinets?.find { it.id == selectedCabinetId }
+ val itemsInCabinet = items.filter { it.cabinetId == selectedCabinetId }
+
+ if (cabinet != null) {
+ CabinetPreviewScreen(
+ cabinet = cabinet,
+ itemsInCabinet = itemsInCabinet,
+ onShelfClick = { shelfId -> selectedShelfId = shelfId },
+ onShowAllItems = { showAllItemsInCabinet = true },
+ onBack = {
+ selectedCabinetId = null
+ showAllItemsInGarage = false
+ }
+ )
+ }
+ }
+ showAllItemsInGarage && selectedGarageId != null -> {
+ val garage = garages.find { it.id == selectedGarageId }
+ val itemsInGarage = items.filter { it.garageId == selectedGarageId }
+
+ if (garage != null) {
+ AllItemsInLocationScreen(
+ locationName = garage.name,
+ locationType = "Garage",
+ items = itemsInGarage,
+ onBack = { showAllItemsInGarage = false }
+ )
+ }
+ }
+ selectedGarageId != null -> {
+ val garage = garages.find { it.id == selectedGarageId }
+ val itemsInGarage = items.filter { it.garageId == selectedGarageId }
+
+ if (garage != null) {
+ GarageCabinetsScreen(
+ garage = garage,
+ itemsInGarage = itemsInGarage,
+ onCabinetClick = { cabinetId -> selectedCabinetId = cabinetId },
+ onShowAllItems = { showAllItemsInGarage = true },
+ onBack = { selectedGarageId = null }
+ )
+ }
+ }
+ else -> {
+ GaragePreviewScreen(
+ garages = garages,
+ items = items,
+ onGarageClick = { garageId -> selectedGarageId = garageId },
+ onBackToEdit = onBackToEdit,
+ onSwitchStyle = onSwitchStyle
+ )
+ }
+ }
+}
+
+// ========================================================================================
+// ALL ITEMS IN LOCATION SCREEN
+// ========================================================================================
+
+@Composable
+private fun AllItemsInLocationScreen(
+ locationName: String,
+ locationType: String,
+ items: List
- ,
+ onBack: () -> Unit
+) {
+ var viewMode by remember { mutableStateOf(ItemViewMode.GRID) }
+
+ Scaffold(
+ topBar = {
+ Surface(
+ shadowElevation = 3.dp,
+ tonalElevation = 2.dp
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.weight(1f)
+ ) {
+ IconButton(
+ onClick = onBack,
+ modifier = Modifier
+ .background(
+ MaterialTheme.colorScheme.surfaceVariant,
+ CircleShape
+ )
+ ) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
+ }
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ "All Items",
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "in $locationName",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Surface(
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f),
+ modifier = Modifier.weight(1f)
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ Icons.Default.Inventory2,
+ null,
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ Column {
+ Text(
+ "${items.size}",
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ Text(
+ "Total Items",
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
+ )
+ }
+ }
+ }
+
+ IconButton(
+ onClick = {
+ viewMode = if (viewMode == ItemViewMode.GRID)
+ ItemViewMode.LIST
+ else
+ ItemViewMode.GRID
+ },
+ modifier = Modifier
+ .background(
+ MaterialTheme.colorScheme.secondaryContainer,
+ RoundedCornerShape(12.dp)
+ )
+ .size(56.dp)
+ ) {
+ Icon(
+ if (viewMode == ItemViewMode.GRID)
+ Icons.Default.ViewAgenda
+ else
+ Icons.Default.ViewModule,
+ "Toggle View",
+ tint = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ }
+ }
+ }
+ }
+ }
+ ) { paddingValues ->
+ if (items.isEmpty()) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Icon(
+ Icons.Default.Inventory,
+ null,
+ modifier = Modifier.size(64.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
+ )
+ Text(
+ "No items in this $locationType",
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ } else {
+ when (viewMode) {
+ ItemViewMode.GRID -> {
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(minSize = 150.dp),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(20.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ items(items) { item ->
+ EnhancedItemCard(item = item)
+ }
+ }
+ }
+ ItemViewMode.LIST -> {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(20.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ items(items) { item ->
+ EnhancedItemListRow(item)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+// ========================================================================================
+// ENHANCED ITEM CARDS
+// ========================================================================================
+
+@Composable
+private fun EnhancedItemCard(item: Item) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(0.8f),
+ elevation = CardDefaults.cardElevation(4.dp),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ if (item.images.isNotEmpty()) {
+ Image(
+ painter = rememberAsyncImagePainter(model = item.images[0].toUri()),
+ contentDescription = item.name,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
+ } else {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(
+ MaterialTheme.colorScheme.surfaceVariant,
+ MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f)
+ )
+ )
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.Default.Inventory2,
+ null,
+ modifier = Modifier.size(64.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
+ )
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxHeight(0.5f)
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.Transparent,
+ Color.Black.copy(alpha = 0.9f)
+ )
+ )
+ )
+ .align(Alignment.BottomCenter)
+ )
+
+ Column(
+ modifier = Modifier
+ .align(Alignment.BottomStart)
+ .padding(14.dp),
+ verticalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Text(
+ item.name,
+ color = Color.White,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.titleMedium,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ if (item.modelNumber != null) {
+ Text(
+ "Model: ${item.modelNumber}",
+ color = Color.White.copy(alpha = 0.9f),
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Surface(
+ shape = RoundedCornerShape(8.dp),
+ color = MaterialTheme.colorScheme.primary
+ ) {
+ Text(
+ "Qty: ${item.quantity}",
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
+ color = Color.White,
+ style = MaterialTheme.typography.labelMedium,
+ fontWeight = FontWeight.Bold
+ )
+ }
+
+ if (item.minPrice != null && item.maxPrice != null) {
+ val avgPrice = (item.minPrice + item.maxPrice) / 2
+ Surface(
+ shape = RoundedCornerShape(8.dp),
+ color = Color(0xFF10B981)
+ ) {
+ Text(
+ "$${String.format(Locale.US, "%.0f", avgPrice)}",
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
+ color = Color.White,
+ style = MaterialTheme.typography.labelMedium,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun EnhancedItemListRow(item: Item) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(3.dp),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ if (item.images.isNotEmpty()) {
+ Image(
+ painter = rememberAsyncImagePainter(model = item.images[0].toUri()),
+ contentDescription = item.name,
+ modifier = Modifier
+ .size(90.dp)
+ .clip(RoundedCornerShape(14.dp)),
+ contentScale = ContentScale.Crop
+ )
+ } else {
+ Box(
+ modifier = Modifier
+ .size(90.dp)
+ .clip(RoundedCornerShape(14.dp))
+ .background(MaterialTheme.colorScheme.surfaceVariant),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.Default.Inventory2,
+ null,
+ modifier = Modifier.size(45.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
+ )
+ }
+ }
+
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Text(
+ item.name,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.titleMedium
+ )
+
+ if (item.modelNumber != null) {
+ Text(
+ "Model: ${item.modelNumber}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Surface(
+ shape = RoundedCornerShape(8.dp),
+ color = MaterialTheme.colorScheme.primaryContainer
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ Icons.Default.Inventory2,
+ null,
+ modifier = Modifier.size(14.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ Text(
+ "${item.quantity}",
+ style = MaterialTheme.typography.labelMedium,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+ }
+
+ if (item.minPrice != null && item.maxPrice != null) {
+ val avgPrice = (item.minPrice + item.maxPrice) / 2
+ Surface(
+ shape = RoundedCornerShape(8.dp),
+ color = Color(0xFF10B981).copy(alpha = 0.15f)
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ Icons.Default.AttachMoney,
+ null,
+ modifier = Modifier.size(14.dp),
+ tint = Color(0xFF10B981)
+ )
+ Text(
+ String.format(Locale.US, "%.0f", avgPrice),
+ style = MaterialTheme.typography.labelMedium,
+ fontWeight = FontWeight.Bold,
+ color = Color(0xFF10B981)
+ )
+ }
+ }
+ }
+ }
+
+ if (item.description != null && item.description.isNotBlank()) {
+ Text(
+ item.description,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ }
+ }
+}
+
+// ========================================================================================
+// GARAGE PREVIEW SCREENS
+// ========================================================================================
+
+@Composable
+private fun GaragePreviewScreen(
+ garages: List,
+ items: List
- ,
+ onGarageClick: (String) -> Unit,
+ onBackToEdit: () -> Unit,
+ onSwitchStyle: () -> Unit
+) {
+ Scaffold(
+ topBar = {
+ Surface(
+ shadowElevation = 3.dp,
+ tonalElevation = 2.dp
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp, vertical = 16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ IconButton(
+ onClick = onSwitchStyle,
+ modifier = Modifier
+ .background(
+ MaterialTheme.colorScheme.secondaryContainer,
+ RoundedCornerShape(12.dp)
+ )
+ ) {
+ Icon(
+ Icons.AutoMirrored.Filled.ViewList,
+ "Switch View",
+ tint = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ }
+ Column {
+ Text(
+ "Your Garages",
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "${garages.size} locations",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ IconButton(
+ onClick = onBackToEdit,
+ modifier = Modifier
+ .background(
+ MaterialTheme.colorScheme.primaryContainer,
+ RoundedCornerShape(12.dp)
+ )
+ ) {
+ Icon(
+ Icons.Default.Edit,
+ "Edit Mode",
+ tint = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+ }
+ }
+ }
+ ) { paddingValues ->
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(minSize = 160.dp),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(20.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ items(garages) { garage ->
+ val itemsInGarage = items.filter { it.garageId == garage.id }
+ val totalValue = itemsInGarage.sumOf {
+ ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 * it.quantity
+ }
+
+ GaragePreviewCard(
+ garage = garage,
+ itemCount = itemsInGarage.size,
+ totalValue = totalValue,
+ onClick = { onGarageClick(garage.id) }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun GaragePreviewCard(
+ garage: Garage,
+ itemCount: Int,
+ totalValue: Double,
+ onClick: () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(0.85f)
+ .clickable(onClick = onClick),
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = 4.dp,
+ pressedElevation = 8.dp
+ ),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
+ ),
+ shape = RoundedCornerShape(20.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(20.dp),
+ verticalArrangement = Arrangement.SpaceBetween
+ ) {
+ Box(
+ modifier = Modifier
+ .size(64.dp)
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(
+ MaterialTheme.colorScheme.primary,
+ MaterialTheme.colorScheme.tertiary
+ )
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.Default.HomeWork,
+ null,
+ modifier = Modifier.size(36.dp),
+ tint = Color.White
+ )
+ }
+
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ Text(
+ garage.name,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.titleLarge,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ InfoChip(
+ icon = Icons.Default.Inventory2,
+ text = itemCount.toString(),
+ modifier = Modifier.weight(1f)
+ )
+ InfoChip(
+ icon = Icons.Default.AttachMoney,
+ text = String.format(Locale.US, "%.0f", totalValue),
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun InfoChip(
+ icon: ImageVector,
+ text: String,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ modifier = modifier,
+ shape = RoundedCornerShape(10.dp),
+ color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ icon,
+ null,
+ modifier = Modifier.size(16.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ Text(
+ text,
+ style = MaterialTheme.typography.labelMedium,
+ fontWeight = FontWeight.SemiBold,
+ color = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+ }
+}
+
+@Composable
+private fun GarageCabinetsScreen(
+ garage: Garage,
+ itemsInGarage: List
- ,
+ onCabinetClick: (String) -> Unit,
+ onShowAllItems: () -> Unit,
+ onBack: () -> Unit
+) {
+ Scaffold(
+ topBar = {
+ Surface(
+ shadowElevation = 3.dp,
+ tonalElevation = 2.dp
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.weight(1f)
+ ) {
+ IconButton(
+ onClick = onBack,
+ modifier = Modifier
+ .background(
+ MaterialTheme.colorScheme.surfaceVariant,
+ CircleShape
+ )
+ ) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
+ }
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ garage.name,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ "${garage.cabinets.size} cabinets • ${itemsInGarage.size} items",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+
+ if (itemsInGarage.isNotEmpty()) {
+ Button(
+ onClick = onShowAllItems,
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(14.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ ),
+ contentPadding = PaddingValues(vertical = 16.dp)
+ ) {
+ Icon(
+ Icons.Default.ViewModule,
+ null,
+ modifier = Modifier.size(24.dp)
+ )
+ Spacer(Modifier.width(12.dp))
+ Text(
+ "View All ${itemsInGarage.size} Items in Garage",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+ }
+ }
+ }
+ ) { paddingValues ->
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(minSize = 160.dp),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(20.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ items(garage.cabinets) { cabinet ->
+ val itemsInCabinet = itemsInGarage.filter { it.cabinetId == cabinet.id }
+ CabinetGridCard(
+ cabinet = cabinet,
+ itemCount = itemsInCabinet.size,
+ shelfCount = cabinet.shelves.size,
+ onClick = { onCabinetClick(cabinet.id) }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun CabinetGridCard(
+ cabinet: Cabinet,
+ itemCount: Int,
+ shelfCount: Int,
+ onClick: () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(0.9f)
+ .clickable(onClick = onClick),
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = 3.dp,
+ pressedElevation = 6.dp
+ ),
+ shape = RoundedCornerShape(18.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(18.dp),
+ verticalArrangement = Arrangement.SpaceBetween
+ ) {
+ Box(
+ modifier = Modifier
+ .size(56.dp)
+ .background(
+ MaterialTheme.colorScheme.secondaryContainer,
+ RoundedCornerShape(14.dp)
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.Default.Kitchen,
+ null,
+ modifier = Modifier.size(32.dp),
+ tint = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ }
+
+ Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
+ Text(
+ cabinet.name,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.titleMedium,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ "$shelfCount shelves • $itemCount items",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun CabinetPreviewScreen(
+ cabinet: Cabinet,
+ itemsInCabinet: List
- ,
+ onShelfClick: (String) -> Unit,
+ onShowAllItems: () -> Unit,
+ onBack: () -> Unit
+) {
+ Scaffold(
+ topBar = {
+ Surface(
+ shadowElevation = 3.dp,
+ tonalElevation = 2.dp
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.weight(1f)
+ ) {
+ IconButton(
+ onClick = onBack,
+ modifier = Modifier
+ .background(
+ MaterialTheme.colorScheme.surfaceVariant,
+ CircleShape
+ )
+ ) {
+ Icon(Icons.Default.ArrowBack, "Back")
+ }
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ cabinet.name,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ "${cabinet.shelves.size} shelves",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+
+ if (itemsInCabinet.isNotEmpty()) {
+ Button(
+ onClick = onShowAllItems,
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(14.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondary
+ ),
+ contentPadding = PaddingValues(vertical = 16.dp)
+ ) {
+ Icon(
+ Icons.Default.ViewModule,
+ null,
+ modifier = Modifier.size(24.dp)
+ )
+ Spacer(Modifier.width(12.dp))
+ Text(
+ "View All ${itemsInCabinet.size} Items in Cabinet",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+ }
+ }
+ }
+ ) { paddingValues ->
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(minSize = 140.dp),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(20.dp),
+ horizontalArrangement = Arrangement.spacedBy(14.dp),
+ verticalArrangement = Arrangement.spacedBy(14.dp)
+ ) {
+ items(cabinet.shelves) { shelf ->
+ val itemsInShelf = itemsInCabinet.filter { it.shelfId == shelf.id }
+ ShelfPreviewCard(
+ shelf = shelf,
+ itemCount = itemsInShelf.size,
+ onClick = { onShelfClick(shelf.id) }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun ShelfPreviewCard(
+ shelf: Shelf,
+ itemCount: Int,
+ onClick: () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(130.dp)
+ .clickable(onClick = onClick),
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = 2.dp,
+ pressedElevation = 4.dp
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.SpaceBetween
+ ) {
+ Box(
+ modifier = Modifier
+ .size(48.dp)
+ .background(
+ MaterialTheme.colorScheme.tertiaryContainer,
+ RoundedCornerShape(12.dp)
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.AutoMirrored.Filled.ViewList,
+ null,
+ modifier = Modifier.size(28.dp),
+ tint = MaterialTheme.colorScheme.onTertiaryContainer
+ )
+ }
+
+ Column {
+ Text(
+ shelf.name,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.titleSmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ "$itemCount items",
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun ShelfPreviewScreen(
+ shelf: Shelf,
+ itemsInShelf: List
- ,
+ boxesInShelf: List,
+ onBoxClick: (String) -> Unit,
+ onShowAllItems: () -> Unit,
+ onBack: () -> Unit
+) {
+ val looseItems = itemsInShelf.filter { it.boxId == null }
+
+ Scaffold(
+ topBar = {
+ Surface(
+ shadowElevation = 3.dp,
+ tonalElevation = 2.dp
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.weight(1f)
+ ) {
+ IconButton(
+ onClick = onBack,
+ modifier = Modifier
+ .background(
+ MaterialTheme.colorScheme.surfaceVariant,
+ CircleShape
+ )
+ ) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
+ }
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ shelf.name,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ "${boxesInShelf.size} boxes • ${looseItems.size} loose items",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+
+ if (itemsInShelf.isNotEmpty()) {
+ Button(
+ onClick = onShowAllItems,
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(14.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.tertiary
+ ),
+ contentPadding = PaddingValues(vertical = 16.dp)
+ ) {
+ Icon(
+ Icons.Default.ViewModule,
+ null,
+ modifier = Modifier.size(24.dp)
+ )
+ Spacer(Modifier.width(12.dp))
+ Text(
+ "View All ${itemsInShelf.size} Items on Shelf",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+ }
+ }
+ }
+ ) { paddingValues ->
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(minSize = 110.dp),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(20.dp),
+ horizontalArrangement = Arrangement.spacedBy(14.dp),
+ verticalArrangement = Arrangement.spacedBy(14.dp)
+ ) {
+ items(looseItems) { item ->
+ SmallItemPreviewCard(item = item)
+ }
+
+ items(boxesInShelf) { box ->
+ val itemsInBox = itemsInShelf.filter { it.boxId == box.id }
+ BoxPreviewCard(
+ box = box,
+ itemCount = itemsInBox.size,
+ onClick = { onBoxClick(box.id) }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun SmallItemPreviewCard(item: Item) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f),
+ elevation = CardDefaults.cardElevation(2.dp),
+ shape = RoundedCornerShape(14.dp)
+ ) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ if (item.images.isNotEmpty()) {
+ Image(
+ painter = rememberAsyncImagePainter(model = item.images[0].toUri()),
+ contentDescription = item.name,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
+ } else {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(
+ MaterialTheme.colorScheme.surfaceVariant,
+ MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f)
+ )
+ )
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.Default.Inventory2,
+ null,
+ modifier = Modifier.size(48.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
+ )
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxHeight(0.6f)
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.Transparent,
+ Color.Black.copy(alpha = 0.85f)
+ )
+ )
+ )
+ .align(Alignment.BottomCenter)
+ )
+
+ Text(
+ item.name,
+ modifier = Modifier
+ .align(Alignment.BottomStart)
+ .padding(10.dp),
+ color = Color.White,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.labelSmall,
+ maxLines = 3,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+}
+
+@Composable
+private fun BoxPreviewCard(
+ box: Box,
+ itemCount: Int,
+ onClick: () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ .clickable(onClick = onClick),
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = 3.dp,
+ pressedElevation = 6.dp
+ ),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF8B5CF6).copy(alpha = 0.15f)
+ ),
+ shape = RoundedCornerShape(14.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(14.dp),
+ verticalArrangement = Arrangement.SpaceBetween,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Box(
+ modifier = Modifier
+ .size(44.dp)
+ .background(
+ Color(0xFF8B5CF6).copy(alpha = 0.3f),
+ RoundedCornerShape(12.dp)
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.Default.Inventory,
+ null,
+ modifier = Modifier.size(26.dp),
+ tint = Color(0xFF8B5CF6)
+ )
+ }
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Text(
+ box.name,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.labelLarge,
+ textAlign = TextAlign.Center,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ "$itemCount items",
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+}
+
+enum class BoxLayout {
+ SCATTERED, NEAT
+}
+
+@Composable
+private fun BoxPreviewScreen(
+ box: Box,
+ itemsInBox: List
- ,
+ boxLayout: BoxLayout,
+ onLayoutChange: (BoxLayout) -> Unit,
+ onBack: () -> Unit
) {
Scaffold(
- floatingActionButton = {
- FloatingActionButton(onClick = onAddGarage) {
- Icon(Icons.Default.Add, contentDescription = "Add Garage")
+ topBar = {
+ Surface(
+ shadowElevation = 3.dp,
+ tonalElevation = 2.dp
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.weight(1f)
+ ) {
+ IconButton(
+ onClick = onBack,
+ modifier = Modifier
+ .background(
+ MaterialTheme.colorScheme.surfaceVariant,
+ CircleShape
+ )
+ ) {
+ Icon(Icons.Default.ArrowBack, "Back")
+ }
+ Column {
+ Text(
+ box.name,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "${itemsInBox.size} items inside",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ LayoutToggleButton(
+ text = "Grid View",
+ isSelected = boxLayout == BoxLayout.SCATTERED,
+ onClick = { onLayoutChange(BoxLayout.SCATTERED) },
+ modifier = Modifier.weight(1f)
+ )
+ LayoutToggleButton(
+ text = "List View",
+ isSelected = boxLayout == BoxLayout.NEAT,
+ onClick = { onLayoutChange(BoxLayout.NEAT) },
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+ }
+ }
+ ) { paddingValues ->
+ if (itemsInBox.isEmpty()) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Icon(
+ Icons.Default.Inventory,
+ null,
+ modifier = Modifier.size(64.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
+ )
+ Text(
+ "No items in this box",
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ } else {
+ when (boxLayout) {
+ BoxLayout.SCATTERED -> ScatteredLayout(itemsInBox, paddingValues)
+ BoxLayout.NEAT -> NeatLayout(itemsInBox, paddingValues)
+ }
+ }
+ }
+}
+
+@Composable
+private fun LayoutToggleButton(
+ text: String,
+ isSelected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Button(
+ onClick = onClick,
+ modifier = modifier.height(48.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = if (isSelected)
+ MaterialTheme.colorScheme.primary
+ else
+ MaterialTheme.colorScheme.surfaceVariant
+ ),
+ shape = RoundedCornerShape(12.dp),
+ elevation = ButtonDefaults.buttonElevation(
+ defaultElevation = if (isSelected) 4.dp else 0.dp
+ )
+ ) {
+ Text(
+ text,
+ fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
+ color = if (isSelected)
+ MaterialTheme.colorScheme.onPrimary
+ else
+ MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+}
+
+@Composable
+private fun ScatteredLayout(items: List
- , paddingValues: PaddingValues) {
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(minSize = 100.dp),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(20.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ items(items) { item ->
+ SmallItemPreviewCard(item = item)
+ }
+ }
+}
+
+@Composable
+private fun NeatLayout(items: List
- , paddingValues: PaddingValues) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(20.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ items(items) { item ->
+ NeatLayoutItemRow(item)
+ }
+ }
+}
+
+@Composable
+private fun NeatLayoutItemRow(item: Item) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(2.dp),
+ shape = RoundedCornerShape(14.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(14.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(14.dp)
+ ) {
+ if (item.images.isNotEmpty()) {
+ Image(
+ painter = rememberAsyncImagePainter(model = item.images[0].toUri()),
+ contentDescription = item.name,
+ modifier = Modifier
+ .size(80.dp)
+ .clip(RoundedCornerShape(12.dp)),
+ contentScale = ContentScale.Crop
+ )
+ } else {
+ Box(
+ modifier = Modifier
+ .size(80.dp)
+ .clip(RoundedCornerShape(12.dp))
+ .background(MaterialTheme.colorScheme.surfaceVariant),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.Default.Inventory2,
+ null,
+ modifier = Modifier.size(40.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
+ )
+ }
+ }
+
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Text(
+ item.name,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.titleMedium
+ )
+ if (item.modelNumber != null) {
+ Text(
+ "Model: ${item.modelNumber}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Text(
+ "Qty: ${item.quantity}",
+ style = MaterialTheme.typography.bodySmall,
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+ }
+ }
+}
+
+// ========================================================================================
+// COLLAPSIBLE LIST PREVIEW
+// ========================================================================================
+
+@Composable
+private fun CollapsibleListPreview(
+ garages: List,
+ items: List
- ,
+ onBackToEdit: () -> Unit,
+ onSwitchStyle: () -> Unit
+) {
+ Scaffold(
+ topBar = {
+ Surface(
+ shadowElevation = 3.dp,
+ tonalElevation = 2.dp
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp, vertical = 16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ IconButton(
+ onClick = onSwitchStyle,
+ modifier = Modifier
+ .background(
+ MaterialTheme.colorScheme.secondaryContainer,
+ RoundedCornerShape(12.dp)
+ )
+ ) {
+ Icon(
+ Icons.Default.GridView,
+ "Switch View",
+ tint = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ }
+ Column {
+ Text(
+ "All Locations",
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "${garages.size} garages • ${items.size} items",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ IconButton(
+ onClick = onBackToEdit,
+ modifier = Modifier
+ .background(
+ MaterialTheme.colorScheme.primaryContainer,
+ RoundedCornerShape(12.dp)
+ )
+ ) {
+ Icon(
+ Icons.Default.Edit,
+ "Edit Mode",
+ tint = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+ }
}
}
) { paddingValues ->
if (garages.isEmpty()) {
- EmptyState(onAddGarage, modifier = Modifier.padding(paddingValues))
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Icon(
+ Icons.Default.LocationCity,
+ null,
+ modifier = Modifier.size(64.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
+ )
+ Text(
+ "No locations to preview",
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
} else {
LazyColumn(
- modifier = Modifier.fillMaxSize().padding(paddingValues),
- contentPadding = PaddingValues(16.dp),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(garages, key = { it.id }) { garage ->
val itemsInGarage = items.filter { it.garageId == garage.id }
- GarageSection(
- garage = garage,
- itemsInGarage = itemsInGarage,
- onAddCabinet = { onAddCabinet(garage.id) },
- onAddShelf = onAddShelf,
- onAddBox = onAddBox,
- onRenameLocation = onRenameLocation,
- onDeleteLocation = onDeleteLocation
+ CollapsibleGarageSection(garage, itemsInGarage)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun CollapsibleGarageSection(garage: Garage, itemsInGarage: List
- ) {
+ var isExpanded by remember { mutableStateOf(false) }
+ val totalValue = itemsInGarage.sumOf { ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 * it.quantity }
+
+ Card(
+ elevation = CardDefaults.cardElevation(3.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
+ ),
+ shape = RoundedCornerShape(18.dp),
+ modifier = Modifier.animateContentSize(
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioMediumBouncy,
+ stiffness = Spring.StiffnessLow
+ )
+ )
+ ) {
+ Column {
+ PreviewSectionHeader(
+ title = garage.name,
+ icon = Icons.Default.HomeWork,
+ isExpanded = isExpanded,
+ onToggleExpand = { isExpanded = !isExpanded }
+ ) {
+ StatChip(icon = Icons.Default.Inventory2, text = "${itemsInGarage.size}", small = false)
+ StatChip(icon = Icons.Default.AttachMoney, text = String.format(Locale.US, "%.0f", totalValue), small = false)
+ }
+
+ AnimatedVisibility(
+ visible = isExpanded,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ Column(
+ modifier = Modifier.padding(start = 18.dp, end = 12.dp, bottom = 16.dp, top = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(14.dp)
+ ) {
+ if (garage.cabinets.isEmpty()) {
+ EmptyChildState("No cabinets in this garage")
+ } else {
+ garage.cabinets.forEach { cabinet ->
+ val itemsInCabinet = itemsInGarage.filter { it.cabinetId == cabinet.id }
+ CollapsibleCabinetSection(cabinet, itemsInCabinet)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun CollapsibleCabinetSection(cabinet: Cabinet, itemsInCabinet: List
- ) {
+ var isExpanded by remember { mutableStateOf(false) }
+
+ Surface(
+ shape = RoundedCornerShape(14.dp),
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f)),
+ color = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
+ modifier = Modifier.animateContentSize(
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioMediumBouncy,
+ stiffness = Spring.StiffnessLow
+ )
+ )
+ ) {
+ Column {
+ PreviewSectionHeader(
+ title = cabinet.name,
+ icon = Icons.Default.Kitchen,
+ isExpanded = isExpanded,
+ onToggleExpand = { isExpanded = !isExpanded }
+ ) {
+ StatChip(icon = Icons.Default.Inventory2, text = "${itemsInCabinet.size}", small = true)
+ }
+
+ AnimatedVisibility(visible = isExpanded) {
+ Column(
+ modifier = Modifier.padding(start = 18.dp, end = 12.dp, bottom = 14.dp, top = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ if (cabinet.shelves.isEmpty()) {
+ EmptyChildState("No shelves in this cabinet")
+ } else {
+ cabinet.shelves.forEach { shelf ->
+ val itemsInShelf = itemsInCabinet.filter { it.shelfId == shelf.id }
+ CollapsibleShelfSection(shelf, itemsInShelf)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun CollapsibleShelfSection(shelf: Shelf, itemsInShelf: List
- ) {
+ var isExpanded by remember { mutableStateOf(false) }
+
+ Surface(
+ shape = RoundedCornerShape(12.dp),
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f)),
+ color = MaterialTheme.colorScheme.background,
+ modifier = Modifier.animateContentSize(
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioMediumBouncy,
+ stiffness = Spring.StiffnessLow
+ )
+ )
+ ) {
+ Column {
+ PreviewSectionHeader(
+ title = shelf.name,
+ icon = Icons.AutoMirrored.Filled.ViewList,
+ isExpanded = isExpanded,
+ onToggleExpand = { isExpanded = !isExpanded }
+ ) {
+ StatChip(icon = Icons.Default.Inventory2, text = "${itemsInShelf.size}", small = true)
+ }
+
+ AnimatedVisibility(visible = isExpanded) {
+ Column(
+ modifier = Modifier.padding(start = 18.dp, end = 12.dp, bottom = 14.dp, top = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ val looseItems = itemsInShelf.filter { it.boxId == null }
+ if (shelf.boxes.isEmpty() && looseItems.isEmpty()) {
+ EmptyChildState("No items or boxes on this shelf")
+ } else {
+ shelf.boxes.forEach { box ->
+ val itemsInBox = itemsInShelf.filter { it.boxId == box.id }
+ CollapsibleBoxSection(box, itemsInBox)
+ }
+ looseItems.forEach { item ->
+ NeatLayoutItemRow(item)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun CollapsibleBoxSection(box: Box, itemsInBox: List
- ) {
+ var isExpanded by remember { mutableStateOf(false) }
+
+ Surface(
+ shape = RoundedCornerShape(12.dp),
+ border = BorderStroke(1.dp, Color(0xFF8B5CF6).copy(alpha = 0.2f)),
+ color = Color(0xFF8B5CF6).copy(alpha = 0.08f),
+ modifier = Modifier
+ .fillMaxWidth()
+ .animateContentSize(
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioMediumBouncy,
+ stiffness = Spring.StiffnessLow
+ )
+ )
+ ) {
+ Column {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { isExpanded = !isExpanded }
+ .padding(14.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(14.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .background(
+ color = Color(0xFF8B5CF6).copy(alpha = 0.2f),
+ shape = RoundedCornerShape(10.dp)
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.Default.Inventory,
+ null,
+ modifier = Modifier.size(22.dp),
+ tint = Color(0xFF8B5CF6)
+ )
+ }
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ box.name,
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "${itemsInBox.size} items",
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
+
+ Icon(
+ imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
+ contentDescription = "Toggle",
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ AnimatedVisibility(visible = isExpanded) {
+ Column(
+ modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 14.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ if (itemsInBox.isEmpty()) {
+ EmptyChildState("No items in this box")
+ } else {
+ itemsInBox.forEach { item ->
+ NeatLayoutItemRow(item)
+ }
+ }
+ }
}
}
}
}
-// ======================================================================
-// SECTION COMPOSABLES
-// ======================================================================
+// ========================================================================================
+// EDIT MODE COMPONENTS
+// ========================================================================================
@Composable
-private fun GarageSection(garage: Garage, itemsInGarage: List
- , onAddCabinet: () -> Unit, onAddShelf: (cabinetId: String) -> Unit, onAddBox: (shelfId: String) -> Unit, onRenameLocation: (id: String, oldName: String, type: String) -> Unit, onDeleteLocation: (id: String, name: String, type: String) -> Unit) {
+private fun GarageSection(
+ garage: Garage,
+ itemsInGarage: List
- ,
+ onAddCabinet: () -> Unit,
+ onAddShelf: (cabinetId: String) -> Unit,
+ onAddBox: (shelfId: String) -> Unit,
+ onRenameLocation: (id: String, oldName: String, type: String) -> Unit,
+ onDeleteLocation: (id: String, name: String, type: String) -> Unit
+) {
var isExpanded by remember { mutableStateOf(false) }
+ var showAddDialog by remember { mutableStateOf(false) }
val totalValue = itemsInGarage.sumOf { ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 * it.quantity }
- Card(elevation = CardDefaults.cardElevation(4.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))) {
+ Card(
+ elevation = CardDefaults.cardElevation(3.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
+ ),
+ shape = RoundedCornerShape(18.dp),
+ modifier = Modifier.animateContentSize(
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioMediumBouncy,
+ stiffness = Spring.StiffnessLow
+ )
+ )
+ ) {
Column {
SectionHeader(
- title = garage.name, icon = Icons.Default.HomeWork, isExpanded = isExpanded,
+ title = garage.name,
+ icon = Icons.Default.HomeWork,
+ isExpanded = isExpanded,
onToggleExpand = { isExpanded = !isExpanded },
- onAddClick = onAddCabinet,
+ onAddClick = { showAddDialog = true },
onRenameClick = { onRenameLocation(garage.id, garage.name, "garage") },
onDeleteClick = { onDeleteLocation(garage.id, garage.name, "garage") }
) {
- StatChip(icon = Icons.Default.Inventory2, text = "${itemsInGarage.size} items")
- StatChip(icon = Icons.Default.AttachMoney, text = "$${String.format(Locale.US, "%.2f", totalValue)}")
+ StatChip(icon = Icons.Default.Inventory2, text = "${itemsInGarage.size}", small = false)
+ StatChip(icon = Icons.Default.AttachMoney, text = String.format(Locale.US, "%.0f", totalValue), small = false)
}
- AnimatedVisibility(visible = isExpanded) {
- Column(modifier = Modifier.padding(start = 24.dp, end = 8.dp, bottom = 8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
+
+ AnimatedVisibility(
+ visible = isExpanded,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ Column(
+ modifier = Modifier.padding(start = 18.dp, end = 12.dp, bottom = 16.dp, top = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(14.dp)
+ ) {
if (garage.cabinets.isEmpty()) {
- EmptyChildState("No cabinets here. Tap '+ Add' to create one.")
+ EmptyChildState("No cabinets yet. Tap '+' to add.")
} else {
garage.cabinets.forEach { cabinet ->
val itemsInCabinet = itemsInGarage.filter { it.cabinetId == cabinet.id }
- CabinetSection(cabinet, itemsInCabinet, { onAddShelf(cabinet.id) }, onAddBox, onRenameLocation, onDeleteLocation)
+ CabinetSection(
+ cabinet,
+ itemsInCabinet,
+ { onAddShelf(cabinet.id) },
+ onAddBox,
+ onRenameLocation,
+ onDeleteLocation
+ )
}
}
}
}
}
}
+
+ if (showAddDialog) {
+ MultiAddDialog(
+ title = "Add Cabinets",
+ onDismiss = { showAddDialog = false },
+ onConfirm = { count ->
+ repeat(count) { onAddCabinet() }
+ }
+ )
+ }
}
@Composable
-private fun CabinetSection(cabinet: Cabinet, itemsInCabinet: List
- , onAddShelf: () -> Unit, onAddBox: (shelfId: String) -> Unit, onRenameLocation: (id: String, oldName: String, type: String) -> Unit, onDeleteLocation: (id: String, name: String, type: String) -> Unit) {
+private fun CabinetSection(
+ cabinet: Cabinet,
+ itemsInCabinet: List
- ,
+ onAddShelf: () -> Unit,
+ onAddBox: (shelfId: String) -> Unit,
+ onRenameLocation: (id: String, oldName: String, type: String) -> Unit,
+ onDeleteLocation: (id: String, name: String, type: String) -> Unit
+) {
var isExpanded by remember { mutableStateOf(false) }
- Surface(shape = MaterialTheme.shapes.medium, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))) {
+ var showAddDialog by remember { mutableStateOf(false) }
+
+ Surface(
+ shape = RoundedCornerShape(14.dp),
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f)),
+ color = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
+ modifier = Modifier.animateContentSize(
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioMediumBouncy,
+ stiffness = Spring.StiffnessLow
+ )
+ )
+ ) {
Column {
SectionHeader(
- title = cabinet.name, icon = Icons.Default.Kitchen, isExpanded = isExpanded,
+ title = cabinet.name,
+ icon = Icons.Default.Kitchen,
+ isExpanded = isExpanded,
onToggleExpand = { isExpanded = !isExpanded },
- onAddClick = onAddShelf,
+ onAddClick = { showAddDialog = true },
onRenameClick = { onRenameLocation(cabinet.id, cabinet.name, "cabinet") },
onDeleteClick = { onDeleteLocation(cabinet.id, cabinet.name, "cabinet") }
) {
- StatChip(icon = Icons.Default.Inventory2, text = "${itemsInCabinet.size} items", small = true)
+ StatChip(icon = Icons.Default.Inventory2, text = "${itemsInCabinet.size}", small = true)
}
- AnimatedVisibility(visible = isExpanded) {
- Column(modifier = Modifier.padding(start = 24.dp, end = 8.dp, bottom = 8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
+
+ AnimatedVisibility(
+ visible = isExpanded,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ Column(
+ modifier = Modifier.padding(start = 18.dp, end = 12.dp, bottom = 14.dp, top = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
if (cabinet.shelves.isEmpty()) {
- EmptyChildState("No shelves in this cabinet.")
+ EmptyChildState("No shelves yet. Tap '+' to add.")
} else {
cabinet.shelves.forEach { shelf ->
val itemsInShelf = itemsInCabinet.filter { it.shelfId == shelf.id }
- ShelfSection(shelf, itemsInShelf, { onAddBox(shelf.id) }, onRenameLocation, onDeleteLocation)
+ ShelfSection(
+ shelf,
+ itemsInShelf,
+ { onAddBox(shelf.id) },
+ onRenameLocation,
+ onDeleteLocation
+ )
}
}
}
}
}
}
+
+ if (showAddDialog) {
+ MultiAddDialog(
+ title = "Add Shelves",
+ onDismiss = { showAddDialog = false },
+ onConfirm = { count ->
+ repeat(count) { onAddShelf() }
+ }
+ )
+ }
}
@Composable
-private fun ShelfSection(shelf: Shelf, itemsInShelf: List
- , onAddBox: () -> Unit, onRenameLocation: (id: String, oldName: String, type: String) -> Unit, onDeleteLocation: (id: String, name: String, type: String) -> Unit) {
+private fun ShelfSection(
+ shelf: Shelf,
+ itemsInShelf: List
- ,
+ onAddBox: () -> Unit,
+ onRenameLocation: (id: String, oldName: String, type: String) -> Unit,
+ onDeleteLocation: (id: String, name: String, type: String) -> Unit
+) {
var isExpanded by remember { mutableStateOf(false) }
- Surface(shape = MaterialTheme.shapes.medium, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f))) {
+ var showAddDialog by remember { mutableStateOf(false) }
+
+ Surface(
+ shape = RoundedCornerShape(12.dp),
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f)),
+ color = MaterialTheme.colorScheme.background,
+ modifier = Modifier.animateContentSize(
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioMediumBouncy,
+ stiffness = Spring.StiffnessLow
+ )
+ )
+ ) {
Column {
SectionHeader(
- title = shelf.name, icon = Icons.AutoMirrored.Filled.ViewList, isExpanded = isExpanded,
+ title = shelf.name,
+ icon = Icons.AutoMirrored.Filled.ViewList,
+ isExpanded = isExpanded,
onToggleExpand = { isExpanded = !isExpanded },
- onAddClick = onAddBox,
+ onAddClick = { showAddDialog = true },
onRenameClick = { onRenameLocation(shelf.id, shelf.name, "shelf") },
onDeleteClick = { onDeleteLocation(shelf.id, shelf.name, "shelf") }
) {
- StatChip(icon = Icons.Default.Inventory2, text = "${itemsInShelf.size} items", small = true)
+ StatChip(icon = Icons.Default.Inventory2, text = "${itemsInShelf.size}", small = true)
}
- AnimatedVisibility(visible = isExpanded) {
- Column(modifier = Modifier.padding(start = 24.dp, end = 8.dp, bottom = 8.dp)) {
+
+ AnimatedVisibility(
+ visible = isExpanded,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ Column(
+ modifier = Modifier.padding(start = 18.dp, end = 12.dp, bottom = 14.dp, top = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
if (shelf.boxes.isEmpty()) {
- EmptyChildState("No boxes on this shelf.")
+ EmptyChildState("No boxes yet. Tap '+' to add.")
} else {
shelf.boxes.forEach { box ->
val itemsInBox = itemsInShelf.filter { it.boxId == box.id }
- BoxItem(box = box, itemCount = itemsInBox.size)
+ BoxItem(
+ box = box,
+ itemCount = itemsInBox.size,
+ onRename = { onRenameLocation(box.id, box.name, "box") },
+ onDelete = { onDeleteLocation(box.id, box.name, "box") }
+ )
}
}
}
}
}
}
-}
+ if (showAddDialog) {
+ MultiAddDialog(
+ title = "Add Boxes",
+ onDismiss = { showAddDialog = false },
+ onConfirm = { count ->
+ repeat(count) { onAddBox() }
+ }
+ )
+ }
+}
@Composable
-private fun SectionHeader(title: String, icon: ImageVector, isExpanded: Boolean, onToggleExpand: () -> Unit, onAddClick: () -> Unit, onRenameClick: () -> Unit, onDeleteClick: () -> Unit, statsContent: @Composable RowScope.() -> Unit) {
+private fun SectionHeader(
+ title: String,
+ icon: ImageVector,
+ isExpanded: Boolean,
+ onToggleExpand: () -> Unit,
+ onAddClick: () -> Unit,
+ onRenameClick: () -> Unit,
+ onDeleteClick: () -> Unit,
+ statsContent: @Composable RowScope.() -> Unit
+) {
var showMenu by remember { mutableStateOf(false) }
- Row(modifier = Modifier.fillMaxWidth().clickable(onClick = onToggleExpand).padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
- Icon(icon, null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.primary)
- Spacer(Modifier.width(12.dp))
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onToggleExpand)
+ .padding(horizontal = 14.dp, vertical = 14.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(42.dp)
+ .background(
+ color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f),
+ shape = RoundedCornerShape(12.dp)
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ icon,
+ null,
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+
Column(modifier = Modifier.weight(1f)) {
- Text(text = title, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
- Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { statsContent() }
+ Text(
+ text = title,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.titleMedium
+ )
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(top = 6.dp)
+ ) {
+ statsContent()
+ }
}
- Row(verticalAlignment = Alignment.CenterVertically) {
- IconButton(onClick = onAddClick) { Icon(Icons.Default.Add, "+ Add") }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ IconButton(
+ onClick = onAddClick,
+ modifier = Modifier
+ .size(40.dp)
+ .background(
+ MaterialTheme.colorScheme.primaryContainer,
+ CircleShape
+ )
+ ) {
+ Icon(
+ Icons.Default.Add,
+ "Add",
+ modifier = Modifier.size(22.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+
Box {
- IconButton(onClick = { showMenu = true }) { Icon(Icons.Default.MoreVert, "More Options") }
- DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
- DropdownMenuItem(text = { Text("Rename") }, onClick = { showMenu = false; onRenameClick() })
- DropdownMenuItem(text = { Text("Delete", color = MaterialTheme.colorScheme.error) }, onClick = { showMenu = false; onDeleteClick() })
+ IconButton(
+ onClick = { showMenu = true },
+ modifier = Modifier.size(40.dp)
+ ) {
+ Icon(
+ Icons.Default.MoreVert,
+ "More Options",
+ modifier = Modifier.size(22.dp)
+ )
+ }
+ DropdownMenu(
+ expanded = showMenu,
+ onDismissRequest = { showMenu = false },
+ ) {
+ DropdownMenuItem(
+ text = { Text("Rename") },
+ leadingIcon = { Icon(Icons.Default.Edit, null, modifier = Modifier.size(20.dp)) },
+ onClick = { showMenu = false; onRenameClick() }
+ )
+ DropdownMenuItem(
+ text = { Text("Delete", color = MaterialTheme.colorScheme.error) },
+ leadingIcon = {
+ Icon(
+ Icons.Default.Delete,
+ null,
+ modifier = Modifier.size(20.dp),
+ tint = MaterialTheme.colorScheme.error
+ )
+ },
+ onClick = { showMenu = false; onDeleteClick() }
+ )
}
}
- Icon(imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, "Toggle Section")
+
+ Icon(
+ imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
+ contentDescription = "Toggle",
+ modifier = Modifier.size(26.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+}
+
+@Composable
+private fun PreviewSectionHeader(
+ title: String,
+ icon: ImageVector,
+ isExpanded: Boolean,
+ onToggleExpand: () -> Unit,
+ statsContent: @Composable RowScope.() -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onToggleExpand)
+ .padding(horizontal = 14.dp, vertical = 14.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(42.dp)
+ .background(
+ color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f),
+ shape = RoundedCornerShape(12.dp)
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ icon,
+ null,
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = title,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.titleMedium
+ )
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(top = 6.dp)
+ ) {
+ statsContent()
+ }
}
+
+ Icon(
+ imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
+ contentDescription = "Toggle",
+ modifier = Modifier.size(26.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
}
}
@Composable
private fun StatChip(icon: ImageVector, text: String, small: Boolean = false) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(icon, null, modifier = Modifier.size(if (small) 12.dp else 14.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
- Spacer(Modifier.width(4.dp))
- Text(text, style = if (small) MaterialTheme.typography.labelSmall else MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ Surface(
+ shape = RoundedCornerShape(8.dp),
+ color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)
+ ) {
+ Icon(
+ icon,
+ null,
+ modifier = Modifier.size(if (small) 16.dp else 18.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ Spacer(Modifier.width(6.dp))
+ Text(
+ text,
+ style = if (small) MaterialTheme.typography.labelMedium else MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.onPrimaryContainer,
+ fontWeight = FontWeight.SemiBold
+ )
+ }
}
}
@Composable
-private fun BoxItem(box: Box, itemCount: Int) {
- Row(Modifier.fillMaxWidth().padding(start = 8.dp, top = 4.dp, bottom = 4.dp), verticalAlignment = Alignment.CenterVertically) {
- Text("└─", color = MaterialTheme.colorScheme.outline); Spacer(Modifier.width(4.dp))
- Icon(Icons.Default.Inventory, null, modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
- Spacer(Modifier.width(8.dp))
- Text(box.name, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
- Text("$itemCount items", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
+private fun BoxItem(
+ box: Box,
+ itemCount: Int,
+ onRename: () -> Unit,
+ onDelete: () -> Unit
+) {
+ var showMenu by remember { mutableStateOf(false) }
+
+ Surface(
+ shape = RoundedCornerShape(12.dp),
+ border = BorderStroke(1.dp, Color(0xFF8B5CF6).copy(alpha = 0.2f)),
+ color = Color(0xFF8B5CF6).copy(alpha = 0.08f),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(14.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(14.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .background(
+ color = Color(0xFF8B5CF6).copy(alpha = 0.2f),
+ shape = RoundedCornerShape(10.dp)
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.Default.Inventory,
+ null,
+ modifier = Modifier.size(22.dp),
+ tint = Color(0xFF8B5CF6)
+ )
+ }
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ box.name,
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "$itemCount items",
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ Box {
+ IconButton(
+ onClick = { showMenu = true },
+ modifier = Modifier.size(36.dp)
+ ) {
+ Icon(
+ Icons.Default.MoreVert,
+ "More",
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ DropdownMenu(
+ expanded = showMenu,
+ onDismissRequest = { showMenu = false },
+ ) {
+ DropdownMenuItem(
+ text = { Text("Rename") },
+ leadingIcon = { Icon(Icons.Default.Edit, null, modifier = Modifier.size(20.dp)) },
+ onClick = { showMenu = false; onRename() }
+ )
+ DropdownMenuItem(
+ text = { Text("Delete", color = MaterialTheme.colorScheme.error) },
+ leadingIcon = {
+ Icon(
+ Icons.Default.Delete,
+ null,
+ modifier = Modifier.size(20.dp),
+ tint = MaterialTheme.colorScheme.error
+ )
+ },
+ onClick = { showMenu = false; onDelete() }
+ )
+ }
+ }
+ }
}
}
@Composable
private fun EmptyState(onAddGarage: () -> Unit, modifier: Modifier = Modifier) {
- Column(modifier = modifier.fillMaxSize().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
- Icon(Icons.Default.LocationCity, null, modifier = Modifier.size(64.dp), tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f))
- Text("No Locations Created Yet", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(top = 16.dp))
- Text("Get started by adding your first garage.", textAlign = TextAlign.Center, modifier = Modifier.padding(top = 8.dp))
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(32.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .size(120.dp)
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(
+ MaterialTheme.colorScheme.primaryContainer,
+ MaterialTheme.colorScheme.tertiaryContainer
+ )
+ ),
+ shape = CircleShape
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.Default.LocationCity,
+ null,
+ modifier = Modifier.size(64.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+
Spacer(Modifier.height(24.dp))
- Button(onClick = onAddGarage) { Icon(Icons.Default.Add, "Add Garage"); Spacer(Modifier.width(8.dp)); Text("Add Your First Garage") }
+
+ Text(
+ "No Storage Locations Yet",
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center
+ )
+
+ Text(
+ "Create your first garage to start organizing your inventory efficiently",
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.bodyLarge
+ )
+
+ Spacer(Modifier.height(32.dp))
+
+ Button(
+ onClick = onAddGarage,
+ shape = RoundedCornerShape(14.dp),
+ contentPadding = PaddingValues(horizontal = 32.dp, vertical = 16.dp),
+ elevation = ButtonDefaults.buttonElevation(4.dp)
+ ) {
+ Icon(Icons.Default.Add, "Add", modifier = Modifier.size(22.dp))
+ Spacer(Modifier.width(10.dp))
+ Text("Create First Garage", style = MaterialTheme.typography.titleSmall)
+ }
}
}
@Composable
private fun EmptyChildState(message: String) {
- Text(message, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(16.dp).fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp)
+ .background(
+ MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
+ RoundedCornerShape(12.dp)
+ )
+ .padding(20.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ message,
+ style = MaterialTheme.typography.bodyMedium,
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/MainAppScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/MainAppScreen.kt
index 52574d6..fdcdb1b 100644
--- a/app/src/main/java/com/samuel/inventorymanager/screens/MainAppScreen.kt
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/MainAppScreen.kt
@@ -1,30 +1,53 @@
package com.samuel.inventorymanager.screens
+import android.Manifest
import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.net.Uri
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.isSystemInDarkTheme
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.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.HelpOutline
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.CameraAlt
+import androidx.compose.material.icons.filled.ChevronRight
+import androidx.compose.material.icons.filled.CloudUpload
import androidx.compose.material.icons.filled.Dashboard
import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.Image
import androidx.compose.material.icons.filled.Inventory2
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.icons.filled.PhotoLibrary
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Sync
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
@@ -33,15 +56,19 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.FloatingActionButtonDefaults
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
-import androidx.compose.material3.NavigationBar
-import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
@@ -49,8 +76,8 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -58,47 +85,87 @@ import androidx.compose.runtime.rememberCoroutineScope
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.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.core.content.ContextCompat
+import androidx.core.content.FileProvider
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
import com.google.gson.Gson
import com.samuel.inventorymanager.data.AppSettings
+import com.samuel.inventorymanager.data.AppTheme
+import com.samuel.inventorymanager.services.OCRService
import com.samuel.inventorymanager.ui.theme.AppThemeType
import com.samuel.inventorymanager.ui.theme.InventoryManagerTheme
import kotlinx.coroutines.launch
import java.io.File
import java.io.InputStreamReader
+import java.util.UUID
-// ========================================================================================
-// SCREEN NAVIGATION DEFINITIONS
-// ========================================================================================
+// ViewModel Factory
+class CreateItemViewModelFactory(
+ private val application: android.app.Application,
+ private val ocrService: OCRService
+) : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ if (modelClass.isAssignableFrom(CreateItemViewModel::class.java)) {
+ return CreateItemViewModel(application, ocrService) as T
+ }
+ throw IllegalArgumentException("Unknown ViewModel class")
+ }
+}
-enum class Screen {
- Dashboard, Items, Locations, Search,
- Overview, Images, History, Settings, Sync, Help,
- CreateItem
+// Navigation Routes
+sealed class AppScreen(val route: String) {
+ object Dashboard : AppScreen("dashboard")
+ object Locations : AppScreen("locations")
+ object Search : AppScreen("search")
+ object Overview : AppScreen("overview")
+ object Images : AppScreen("images")
+ object History : AppScreen("history")
+ object Settings : AppScreen("settings")
+ object Sync : AppScreen("sync")
+ object Help : AppScreen("help")
+ object CreateItem : AppScreen("create_item")
+ object ShareScreen : AppScreen("share")
+
+ object Drive : AppScreen("google sync") // ADD THIS
+ // Image flow screens
+ object ImageEdit : AppScreen("image_edit")
+ object AIProcessing : AppScreen("ai_processing")
+ object AIResults : AppScreen("ai_results")
}
data class NavGridItem(
val title: String,
val icon: ImageVector,
- val screen: Screen
+ val screen: AppScreen
)
val navigationItems = listOf(
- NavGridItem("Dashboard", Icons.Default.Dashboard, Screen.Dashboard),
- NavGridItem("Items", Icons.Default.Inventory2, Screen.CreateItem),
- NavGridItem("Locations", Icons.Default.LocationOn, Screen.Locations),
- NavGridItem("Search", Icons.Default.Search, Screen.Search),
- NavGridItem("Overview", Icons.AutoMirrored.Filled.List, Screen.Overview),
- NavGridItem("Images", Icons.Default.Image, Screen.Images),
- NavGridItem("History", Icons.Default.History, Screen.History),
- NavGridItem("Settings", Icons.Default.Settings, Screen.Settings),
- NavGridItem("Sync", Icons.Default.Sync, Screen.Sync),
- NavGridItem("Help", Icons.AutoMirrored.Filled.HelpOutline, Screen.Help)
+ NavGridItem("Dashboard", Icons.Default.Dashboard, AppScreen.Dashboard),
+ NavGridItem("Create Item", Icons.Default.Inventory2, AppScreen.CreateItem),
+ NavGridItem("Locations", Icons.Default.LocationOn, AppScreen.Locations),
+ NavGridItem("Search", Icons.Default.Search, AppScreen.Search),
+ NavGridItem("Overview", Icons.AutoMirrored.Filled.List, AppScreen.Overview),
+ NavGridItem("Images", Icons.Default.Image, AppScreen.Images),
+ NavGridItem("Share Screen", Icons.Default.Share, AppScreen.ShareScreen),
+ NavGridItem("History", Icons.Default.History, AppScreen.History),
+ NavGridItem("Google Sync" , Icons.Default.CloudUpload, AppScreen.Drive),
+ NavGridItem("Settings", Icons.Default.Settings, AppScreen.Settings),
+ NavGridItem("Sync (COMING SOON)", Icons.Default.Sync, AppScreen.Sync),
+ NavGridItem("Help", Icons.AutoMirrored.Filled.HelpOutline, AppScreen.Help)
)
sealed class DialogState {
@@ -112,20 +179,39 @@ sealed class DialogState {
object ClearHistory : DialogState()
}
-val LocalAppSettings = compositionLocalOf { AppSettings() }
-val LocalOnSettingsChange = compositionLocalOf<(AppSettings) -> Unit> { {} }
-
-// ========================================================================================
-// MAIN APPLICATION SCREEN
-// ========================================================================================
+@Composable
+fun mapAppThemeToAppThemeType(appTheme: AppTheme): AppThemeType {
+ return when (appTheme) {
+ AppTheme.LIGHT -> AppThemeType.LIGHT
+ AppTheme.DARK -> AppThemeType.DARK
+ AppTheme.SYSTEM -> if (isSystemInDarkTheme()) AppThemeType.DARK else AppThemeType.LIGHT
+ AppTheme.DRACULA -> AppThemeType.DRACULA
+ AppTheme.VAMPIRE -> AppThemeType.VAMPIRE
+ AppTheme.OCEAN -> AppThemeType.OCEAN
+ AppTheme.FOREST -> AppThemeType.FOREST
+ AppTheme.SUNSET -> AppThemeType.SUNSET
+ AppTheme.CYBERPUNK -> AppThemeType.CYBERPUNK
+ AppTheme.NEON -> AppThemeType.NEON
+ AppTheme.CUSTOM -> AppThemeType.LIGHT
+ }
+}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainAppScreen() {
val context = LocalContext.current
+ val navController = rememberNavController()
+ val scope = rememberCoroutineScope()
+
+ // Factory for ViewModel injection
+ val ocrService = remember { OCRService(context) }
+ val createItemViewModel: CreateItemViewModel = viewModel(
+ factory = CreateItemViewModelFactory(context.applicationContext as android.app.Application, ocrService)
+ )
+
+ // Settings
var appSettings by remember { mutableStateOf(AppSettings()) }
- // Load settings on first launch
LaunchedEffect(Unit) {
appSettings = loadSettingsFromFile(context)
}
@@ -135,75 +221,183 @@ fun MainAppScreen() {
saveSettingsToFile(context, newSettings)
}
- // Determine theme type based on appSettings
val themeType = when {
- appSettings.customTheme != null -> AppThemeType.LIGHT // Default to light for custom
- else -> AppThemeType.LIGHT
+ appSettings.customTheme != null -> AppThemeType.LIGHT
+ else -> mapAppThemeToAppThemeType(appSettings.theme)
}
- val fontScale = appSettings.customTheme?.fontSizeScale ?: 1.0f
+ val fontScale = appSettings.customTheme?.fontSizeScale ?: appSettings.fontSize.scale
- InventoryManagerTheme(
- themeType = themeType,
- fontScale = fontScale
- ) {
- var currentScreen by remember { mutableStateOf(Screen.Dashboard) }
+ InventoryManagerTheme(themeType = themeType, fontScale = fontScale) {
+ // Data State
val garages = remember { mutableStateListOf() }
val items = remember { mutableStateListOf
- () }
val history = remember { mutableStateListOf() }
var dialogState by remember { mutableStateOf(DialogState.Closed) }
+ var saveCounter by remember { mutableIntStateOf(0) }
- val createItemViewModel: CreateItemViewModel = viewModel()
+ // Image flow state
+ var capturedImageUri by remember { mutableStateOf(null) }
+ var editedBitmap by remember { mutableStateOf(null) }
+ var aiAnalysisResult by remember { mutableStateOf(null) }
+ var showAddItemSheet by remember { mutableStateOf(false) }
+
+ // Load data
LaunchedEffect(Unit) {
loadDataFromFile(context, garages, items, history)
}
- val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
- val scope = rememberCoroutineScope()
+ // Auto-save
+ LaunchedEffect(saveCounter) {
+ if (saveCounter > 0) {
+ saveDataToFile(context, AppData(garages.toList(), items.toList(), history.toList()))
+ }
+ }
+
+ val triggerSave = { saveCounter++ }
- val onSaveItem: (Item) -> Unit = { newItem ->
- val index = items.indexOfFirst { it.id == newItem.id }
- val newHistoryEntry: HistoryEntry
+ // Item operations
+ val onSaveItem: (Item) -> Unit = { item ->
+ val index = items.indexOfFirst { it.id == item.id }
if (index != -1) {
- newHistoryEntry = HistoryEntry(
+ items[index] = item
+ history.add(0, HistoryEntry(
id = "hist_${System.currentTimeMillis()}",
- itemId = newItem.id,
- itemName = newItem.name,
- action = HistoryAction.Updated,
+ itemId = item.id,
+ itemName = item.name,
+ actionType = "Updated",
description = "Item details were modified."
- )
- items[index] = newItem
+ ))
} else {
- newHistoryEntry = HistoryEntry(
+ items.add(item)
+ history.add(0, HistoryEntry(
id = "hist_${System.currentTimeMillis()}",
- itemId = newItem.id,
- itemName = newItem.name,
- action = HistoryAction.Added,
+ itemId = item.id,
+ itemName = item.name,
+ actionType = "Added",
description = "A new item was created."
- )
- items.add(newItem)
+ ))
+ }
+ triggerSave()
+ }
+
+ val onUpdateItem: (Item) -> Unit = { updatedItem ->
+ val index = items.indexOfFirst { it.id == updatedItem.id }
+ if (index != -1) {
+ items[index] = updatedItem
+ history.add(0, HistoryEntry(
+ id = "hist_${System.currentTimeMillis()}",
+ itemId = updatedItem.id,
+ itemName = updatedItem.name,
+ actionType = "Updated",
+ description = "Item was updated."
+ ))
+ triggerSave()
+ }
+ }
+
+ val onDeleteItem: (Item) -> Unit = { itemToDelete ->
+ items.remove(itemToDelete)
+ history.add(0, HistoryEntry(
+ id = "hist_${System.currentTimeMillis()}",
+ itemId = itemToDelete.id,
+ itemName = itemToDelete.name,
+ actionType = "Deleted",
+ description = "Item was permanently deleted."
+ ))
+ triggerSave()
+ }
+ val onDeleteHistoryEntry: (HistoryEntry) -> Unit = { entry ->
+ history.remove(entry)
+ triggerSave()
+ }
+
+
+ // Camera & Gallery Launchers
+ val cameraLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.TakePicture()
+ ) { success ->
+ if (success && capturedImageUri != null) {
+ navController.navigate(AppScreen.ImageEdit.route)
}
- history.add(0, newHistoryEntry)
- saveDataToFile(context, AppData(garages.toList(), items.toList(), history.toList()))
}
- val onClearHistory = { dialogState = DialogState.ClearHistory }
+ val galleryLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.GetContent()
+ ) { uri ->
+ uri?.let {
+ capturedImageUri = it
+ navController.navigate(AppScreen.ImageEdit.route)
+ }
+ }
+ val cameraPermissionLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission()
+ ) { granted ->
+ if (granted) {
+ val uri = createImageUri(context)
+ capturedImageUri = uri
+ cameraLauncher.launch(uri)
+ }
+ }
+
+ // Drawer state
+ val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
+ val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
+ val currentAppData = AppData(garages = garages, items = items, history = history)
+ // --- CALLBACK FUNCTIONS FOR SETTINGS SCREEN ---
+ val onDataChange: (AppData) -> Unit = { importedData ->
+ // This runs when you import a file from settings
+ garages.clear()
+ items.clear()
+ history.clear()
+
+ garages.addAll(importedData.garages)
+ items.addAll(importedData.items)
+ history.addAll(importedData.history)
+
+ triggerSave() // Save the new imported data
+ }
+
+ val onClearAllData: () -> Unit = {
+ // This runs when you click "Clear All Data" in settings
+ garages.clear()
+ items.clear()
+ history.clear()
+
+ // Also delete the physical file
+ val file = File(context.filesDir, "app_data.json")
+ if (file.exists()) {
+ file.delete()
+ }
+
+ triggerSave() // Save the empty state
+ }
+
+ // Hide bottom bar on image flow screens
+ val hideBottomBar = currentRoute in listOf(
+ AppScreen.ImageEdit.route,
+ AppScreen.AIProcessing.route,
+ AppScreen.AIResults.route
+ )
+
+ // Dialogs
HandleDialogs(
dialogState = dialogState,
garages = garages,
- onClearHistory = {
- history.clear()
+ onDismiss = {
dialogState = DialogState.Closed
- saveDataToFile(context, AppData(garages.toList(), items.toList(), history.toList()))
+ triggerSave()
},
- onDismiss = {
+ onClearHistory = {
+ history.clear()
dialogState = DialogState.Closed
- saveDataToFile(context, AppData(garages.toList(), items.toList(), history.toList()))
+ triggerSave()
}
)
+ // Main UI
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
@@ -222,9 +416,24 @@ fun MainAppScreen() {
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(navigationItems) { item ->
- GridNavigationButton(item, currentScreen == item.screen) {
- currentScreen = item.screen
- scope.launch { drawerState.close() }
+ GridNavigationButton(
+ item = item,
+ isSelected = currentRoute == item.screen.route
+ ) {
+ // Auto-open camera when clicking "Items"
+ if (item.screen == AppScreen.CreateItem) {
+ scope.launch { drawerState.close() }
+ showAddItemSheet = true // This opens camera/gallery sheet
+ } else {
+ navController.navigate(item.screen.route) {
+ popUpTo(navController.graph.findStartDestination().id) {
+ saveState = true
+ }
+ launchSingleTop = true
+ restoreState = true
+ }
+ scope.launch { drawerState.close() }
+ }
}
}
}
@@ -234,106 +443,560 @@ fun MainAppScreen() {
) {
Scaffold(
topBar = {
- TopAppBar(
- title = {
- Text(
- currentScreen.name,
- style = MaterialTheme.typography.titleLarge,
- fontWeight = FontWeight.Bold
+ if (!hideBottomBar) {
+ TopAppBar(
+ title = {
+ Text(
+ getScreenTitle(currentRoute),
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ navigationIcon = {
+ IconButton({ scope.launch { drawerState.open() } }) {
+ Icon(Icons.Default.Menu, "Menu")
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
- },
- navigationIcon = {
- IconButton({ scope.launch { drawerState.open() } }) {
- Icon(Icons.Default.Menu, "Menu")
- }
- },
- colors = TopAppBarDefaults.topAppBarColors(
- containerColor = MaterialTheme.colorScheme.primaryContainer,
- titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
- )
+ }
},
bottomBar = {
- NavigationBar {
- NavigationBarItem(
- icon = { Icon(Icons.Default.LocationOn, null) },
- label = { Text("Locations") },
- selected = currentScreen == Screen.Locations,
- onClick = { currentScreen = Screen.Locations }
- )
- NavigationBarItem(
- icon = { Icon(Icons.Default.Add, null) },
- label = { Text("Add Item") },
- selected = currentScreen == Screen.CreateItem,
- onClick = {
- createItemViewModel.clearFormForNewItem(garages)
- currentScreen = Screen.CreateItem
- }
- )
- NavigationBarItem(
- icon = { Icon(Icons.AutoMirrored.Filled.List, null) },
- label = { Text("Overview") },
- selected = currentScreen == Screen.Overview,
- onClick = { currentScreen = Screen.Overview }
+ if (!hideBottomBar) {
+ ModernBottomNavBar(
+ currentRoute = currentRoute,
+ onNavigate = { route ->
+ navController.navigate(route) {
+ popUpTo(navController.graph.findStartDestination().id) {
+ saveState = true
+ }
+ launchSingleTop = true
+ restoreState = true
+ }
+ },
+ onAddItem = { showAddItemSheet = true }
)
}
}
) { paddingValues ->
- Box(modifier = Modifier.padding(paddingValues).fillMaxSize()) {
- when (currentScreen) {
- Screen.Dashboard -> DashboardScreen(items, garages)
- Screen.Locations -> LocationsScreen(
- garages = garages,
- items = items,
- onAddGarage = { dialogState = DialogState.AddGarage },
- onAddCabinet = { gid -> dialogState = DialogState.AddCabinet(gid) },
- onAddShelf = { cid -> dialogState = DialogState.AddShelf(cid) },
- onAddBox = { sid -> dialogState = DialogState.AddBox(sid) },
- onRenameLocation = { id, old, type ->
- dialogState = DialogState.RenameLocation(id, old, type)
- },
- onDeleteLocation = { id, name, type ->
- dialogState = DialogState.DeleteLocation(id, name, type)
+ Box(
+ modifier = Modifier
+ .padding(paddingValues)
+ .fillMaxSize()
+ ) {
+ NavHost(
+ navController = navController,
+ startDestination = AppScreen.Dashboard.route
+ ) {
+ composable(AppScreen.Dashboard.route) {
+ DashboardScreen(items, garages)
+ }
+ composable(AppScreen.ShareScreen.route) {
+ ShareScreen(
+ garages = garages,
+ items = items,
+ history = history,
+ onDataImported = { importedData ->
+ garages.clear()
+ items.clear()
+ history.clear()
+
+ garages.addAll(importedData.garages)
+ items.addAll(importedData.items)
+ history.addAll(importedData.history)
+
+ triggerSave()
+
+ navController.popBackStack()
+ }
+ )
+ }
+
+ composable(AppScreen.Drive.route) {
+ GoogleSyncScreen(
+ garages = garages,
+ items = items,
+ history = history,
+ onDataRestored = { restoredData ->
+ garages.clear()
+ garages.addAll(restoredData.garages)
+ items.clear()
+ items.addAll(restoredData.items)
+ history.clear()
+ history.addAll(restoredData.history)
+ triggerSave()
+ }
+ )
+ }
+
+ composable(AppScreen.Locations.route) {
+ LocationsScreen(
+ garages = garages,
+ items = items,
+ onAddGarage = { dialogState = DialogState.AddGarage },
+ onAddCabinet = { gid -> dialogState = DialogState.AddCabinet(gid) },
+ onAddShelf = { cid -> dialogState = DialogState.AddShelf(cid) },
+ onAddBox = { sid -> dialogState = DialogState.AddBox(sid) },
+ onRenameLocation = { id, old, type ->
+ dialogState = DialogState.RenameLocation(id, old, type)
+ },
+ onDeleteLocation = { id, name, type ->
+ dialogState = DialogState.DeleteLocation(id, name, type)
+ }
+ )
+ }
+
+ composable(AppScreen.CreateItem.route) {
+ CreateItemScreen(
+ items = items,
+ garages = garages,
+ onSaveItem = onSaveItem,
+ onUpdateItem = onUpdateItem,
+ viewModel = createItemViewModel,
+ appSettings = appSettings,
+ onDeleteItem = onDeleteItem,
+ onSettingsChange = onSettingsChange
+ )
+ }
+
+ composable(AppScreen.Overview.route) {
+ OverviewScreen(items, garages)
+ }
+
+ composable(AppScreen.Search.route) {
+ SearchScreen(
+ items = items,
+ garages = garages,
+ onEditItem = { item ->
+ createItemViewModel.loadItemForEditing(item, garages)
+ navController.navigate(AppScreen.CreateItem.route)
+ },
+ onDeleteItem = { item ->
+ onDeleteItem(item)
+ },
+ onDuplicateItem = { item ->
+ val duplicatedItem = item.copy(
+ id = UUID.randomUUID().toString(),
+ name = "${item.name} (Copy)"
+ )
+ onSaveItem(duplicatedItem)
+ },
+ onShareItem = { item ->
+ shareItem(context, item, garages)
+ }
+ )
+ }
+
+ composable(AppScreen.Settings.route) {
+ SettingsScreen(
+ currentSettings = appSettings,
+ onSettingsChange = { newSettings -> appSettings = newSettings },
+
+ // Add these three required parameters
+ currentData = currentAppData,
+ onDataChange = onDataChange,
+ onClearAllData = onClearAllData
+ )
+ }
+
+ composable(AppScreen.Help.route) {
+ HelpScreen()
+ }
+
+ composable(AppScreen.Images.route) {
+ ImagesScreen(items) { item ->
+ createItemViewModel.loadItemForEditing(item, garages)
+ navController.navigate(AppScreen.CreateItem.route)
}
- )
- Screen.Items, Screen.CreateItem -> CreateItemScreen(
- garages = garages,
- onSaveItem = onSaveItem,
- viewModel = createItemViewModel,
- appSettings = appSettings,
- onSettingsChange = onSettingsChange
- )
- Screen.Overview -> OverviewScreen(items, garages)
- Screen.Search -> SearchScreen(items, garages)
- Screen.Settings -> SettingsScreen(
- currentSettings = appSettings,
- onSettingsChange = onSettingsChange
- )
- Screen.Help -> HelpScreen()
- Screen.Images -> ImagesScreen(items) { item ->
- createItemViewModel.loadItemForEditing(item, garages)
- currentScreen = Screen.CreateItem
}
- Screen.History -> HistoryScreen(
- history = history,
- items = items,
- onItemClick = { targetItem ->
- createItemViewModel.loadItemForEditing(targetItem, garages)
- currentScreen = Screen.CreateItem
- },
- onClearHistory = onClearHistory
- )
- else -> PlaceholderScreen(screenName = currentScreen.name)
+
+ composable(AppScreen.History.route) {
+ HistoryScreen(
+ history = history,
+ items = items,
+ onItemClick = { targetItem ->
+ createItemViewModel.loadItemForEditing(targetItem, garages)
+ navController.navigate(AppScreen.CreateItem.route)
+ },
+ onClearHistory = {
+ dialogState = DialogState.ClearHistory
+ },
+ onDeleteHistoryEntry = onDeleteHistoryEntry
+ )
+ }
+
+
+ composable(AppScreen.Sync.route) {
+ PlaceholderScreen("Sync")
+ }
+
+ // Image Edit Screen
+ composable(AppScreen.ImageEdit.route) {
+ capturedImageUri?.let { uri ->
+ ImageEditScreen(
+ imageUri = uri,
+ onNext = { bitmap, choice ->
+ editedBitmap = bitmap
+
+ when (choice) {
+ ProcessingChoice.MANUAL -> {
+ val savedUri = saveBitmapToUri(context, bitmap)
+ createItemViewModel.clearFormForNewItem()
+ if(!createItemViewModel.imageUris.contains(savedUri)) {
+ createItemViewModel.imageUris.add(savedUri)
+ }
+
+ navController.navigate(AppScreen.CreateItem.route) {
+ popUpTo(AppScreen.Dashboard.route)
+ }
+ }
+ ProcessingChoice.AI_OCR -> {
+ navController.navigate(AppScreen.AIProcessing.route)
+ }
+ }
+ },
+ onCancel = {
+ capturedImageUri = null
+ editedBitmap = null
+ navController.popBackStack()
+ }
+ )
+ }
+ }
+
+ // AI Processing Screen
+ composable(AppScreen.AIProcessing.route) {
+ editedBitmap?.let { bitmap ->
+ AIProcessingScreen(
+ bitmap = bitmap,
+ onComplete = { result ->
+ aiAnalysisResult = result
+ navController.navigate(AppScreen.AIResults.route)
+ },
+ onError = { _ ->
+ scope.launch {
+ navController.popBackStack()
+ }
+ }
+ )
+ }
+ }
+
+ // AI Results Screen
+ composable(AppScreen.AIResults.route) {
+ editedBitmap?.let { bitmap ->
+ aiAnalysisResult?.let { result ->
+ AIResultsScreen(
+ bitmap = bitmap,
+ result = result,
+ onBackToEdit = {
+ navController.navigate(AppScreen.ImageEdit.route) {
+ popUpTo(AppScreen.ImageEdit.route) { inclusive = true }
+ }
+ },
+ onSaveAndContinue = { finalResult ->
+ val savedUri = saveBitmapToUri(context, bitmap)
+ createItemViewModel.clearFormForNewItem()
+
+ // Prefill from AI result using ViewModel properties
+ finalResult.itemName?.let { createItemViewModel.itemName = it }
+ finalResult.description?.let { createItemViewModel.description = it }
+ finalResult.modelNumber?.let { createItemViewModel.modelNumber = it }
+ finalResult.dimensions?.let { createItemViewModel.dimensions = it }
+ finalResult.estimatedPrice?.let { price ->
+ createItemViewModel.minPrice = price.toString()
+ createItemViewModel.maxPrice = (price * 1.2).toString()
+ }
+
+ if(!createItemViewModel.imageUris.contains(savedUri)) {
+ createItemViewModel.imageUris.add(savedUri)
+ }
+
+ navController.navigate(AppScreen.CreateItem.route) {
+ popUpTo(AppScreen.Dashboard.route)
+ }
+
+ capturedImageUri = null
+ editedBitmap = null
+ aiAnalysisResult = null
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Add Item Bottom Sheet
+ if (showAddItemSheet) {
+ AddItemBottomSheet(
+ onDismiss = { showAddItemSheet = false },
+ onCameraClick = {
+ showAddItemSheet = false
+ if (ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.CAMERA
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ val uri = createImageUri(context)
+ capturedImageUri = uri
+ cameraLauncher.launch(uri)
+ } else {
+ cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
+ },
+ onGalleryClick = {
+ showAddItemSheet = false
+ galleryLauncher.launch("image/*")
+ }
+ )
+ }
+ }
+}
+
+// Modern Bottom Navigation Bar
+@Composable
+fun ModernBottomNavBar(
+ currentRoute: String?,
+ onNavigate: (String) -> Unit,
+ onAddItem: () -> Unit
+) {
+ Surface(
+ shadowElevation = 8.dp,
+ tonalElevation = 3.dp
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp)
+ .background(MaterialTheme.colorScheme.surface),
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ BottomNavItem(
+ icon = Icons.Default.LocationOn,
+ label = "Locations",
+ selected = currentRoute == AppScreen.Locations.route,
+ onClick = { onNavigate(AppScreen.Locations.route) }
+ )
+
+ Box(
+ modifier = Modifier
+ .size(64.dp)
+ .offset(y = (-8).dp)
+ ) {
+ FloatingActionButton(
+ onClick = onAddItem,
+ containerColor = MaterialTheme.colorScheme.primary,
+ elevation = FloatingActionButtonDefaults.elevation(8.dp)
+ ) {
+ Icon(
+ Icons.Default.Add,
+ contentDescription = "Add Item",
+ modifier = Modifier.size(32.dp)
+ )
}
}
+
+ BottomNavItem(
+ icon = Icons.AutoMirrored.Filled.List,
+ label = "Overview",
+ selected = currentRoute == AppScreen.Overview.route,
+ onClick = { onNavigate(AppScreen.Overview.route) }
+ )
}
}
}
-// ========================================================================================
-// HELPER COMPOSABLES
-// ========================================================================================
+@Composable
+fun BottomNavItem(
+ icon: ImageVector,
+ label: String,
+ selected: Boolean,
+ onClick: () -> Unit
+) {
+ val color by animateColorAsState(
+ targetValue = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
+ label = "nav_color"
+ )
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ modifier = Modifier
+ .clickable(onClick = onClick)
+ .padding(8.dp)
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = label,
+ tint = color,
+ modifier = Modifier.size(24.dp)
+ )
+ Spacer(Modifier.height(4.dp))
+ Text(
+ text = label,
+ fontSize = 12.sp,
+ fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal,
+ color = color
+ )
+ }
+}
+
+// Add Item Bottom Sheet
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AddItemBottomSheet(
+ onDismiss: () -> Unit,
+ onCameraClick: () -> Unit,
+ onGalleryClick: () -> Unit
+) {
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ containerColor = Color(0xFF1E293B),
+ shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(24.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Surface(
+ shape = RoundedCornerShape(8.dp),
+ color = Color.White.copy(alpha = 0.3f),
+ modifier = Modifier
+ .width(40.dp)
+ .height(4.dp)
+ ) {}
+
+ Spacer(Modifier.height(16.dp))
+
+ Text(
+ "📸 Add New Item",
+ color = Color.White,
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold
+ )
+
+ Text(
+ "Choose how to add your item",
+ color = Color.White.copy(alpha = 0.7f),
+ fontSize = 14.sp,
+ modifier = Modifier.padding(top = 4.dp)
+ )
+ }
+
+ HorizontalDivider(
+ color = Color.White.copy(alpha = 0.1f),
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+
+ AddItemOptionCard(
+ icon = Icons.Default.CameraAlt,
+ title = "Take Photo",
+ description = "Use your camera to capture the item",
+ color = Color(0xFF8B5CF6),
+ onClick = onCameraClick
+ )
+
+ AddItemOptionCard(
+ icon = Icons.Default.PhotoLibrary,
+ title = "Choose from Gallery",
+ description = "Select an existing photo",
+ color = Color(0xFF10B981),
+ onClick = onGalleryClick
+ )
+
+ OutlinedButton(
+ onClick = onDismiss,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp),
+ shape = RoundedCornerShape(16.dp),
+ border = BorderStroke(2.dp, Color.White.copy(alpha = 0.3f)),
+ colors = ButtonDefaults.outlinedButtonColors(
+ contentColor = Color.White
+ )
+ ) {
+ Text("Cancel", fontWeight = FontWeight.Bold, fontSize = 16.sp)
+ }
+
+ Spacer(Modifier.height(8.dp))
+ }
+ }
+}
+
+@Composable
+fun AddItemOptionCard(
+ icon: ImageVector,
+ title: String,
+ description: String,
+ color: Color,
+ onClick: () -> Unit
+) {
+ Card(
+ onClick = onClick,
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(20.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = color.copy(alpha = 0.15f)
+ ),
+ border = BorderStroke(2.dp, color.copy(alpha = 0.3f))
+ ) {
+ Row(
+ modifier = Modifier.padding(20.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Surface(
+ shape = CircleShape,
+ color = color,
+ modifier = Modifier.size(56.dp)
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ Icon(
+ icon,
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(28.dp)
+ )
+ }
+ }
+ Column(
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ modifier = Modifier.weight(1f)
+ ) {
+ Text(
+ title,
+ color = Color.White,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ description,
+ color = Color.White.copy(alpha = 0.7f),
+ fontSize = 13.sp
+ )
+ }
+ Icon(
+ Icons.Default.ChevronRight,
+ contentDescription = null,
+ tint = Color.White.copy(alpha = 0.5f),
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ }
+}
@Composable
private fun GridNavigationButton(item: NavGridItem, isSelected: Boolean, onClick: () -> Unit) {
@@ -398,256 +1061,393 @@ fun PlaceholderScreen(screenName: String) {
}
}
-@OptIn(ExperimentalMaterial3Api::class)
+// Helper function to share an item
+fun shareItem(context: Context, item: Item, garages: List) {
+ val locationPath = buildLocationPath(item, garages)
+
+ val shareText = buildString {
+ appendLine("📦 ${item.name}")
+ item.modelNumber?.let { appendLine("Model: $it") }
+ appendLine()
+ item.description?.let {
+ appendLine("Description:")
+ appendLine(it)
+ appendLine()
+ }
+ appendLine("📍 Location: $locationPath")
+ appendLine("Quantity: ${item.quantity}")
+ appendLine("Condition: ${item.condition}")
+ item.dimensions?.let { appendLine("Dimensions: $it") }
+
+ if (item.minPrice != null || item.maxPrice != null) {
+ appendLine()
+ append("💰 Price: ")
+ when {
+ item.minPrice != null && item.maxPrice != null ->
+ appendLine("$${item.minPrice} - $${item.maxPrice}")
+ item.minPrice != null -> appendLine("$${item.minPrice}")
+ else -> appendLine("Up to $${item.maxPrice}")
+ }
+ }
+ }
+
+ val sendIntent = Intent().apply {
+ action = Intent.ACTION_SEND
+ putExtra(Intent.EXTRA_TEXT, shareText)
+ putExtra(Intent.EXTRA_TITLE, "Item: ${item.name}")
+ type = "text/plain"
+ }
+
+ val shareIntent = Intent.createChooser(sendIntent, "Share ${item.name}")
+ context.startActivity(shareIntent)
+}
+
+// Helper function for location path
+private fun buildLocationPath(item: Item, garages: List): String {
+ val garage = garages.find { it.id == item.garageId }
+ val cabinet = garage?.cabinets?.find { it.id == item.cabinetId }
+ val shelf = cabinet?.shelves?.find { it.id == item.shelfId }
+ val box = shelf?.boxes?.find { it.id == item.boxId }
+
+ return buildString {
+ garage?.let { append(it.name) }
+ cabinet?.let { append(" → ${it.name}") }
+ shelf?.let { append(" → ${it.name}") }
+ box?.let { append(" → ${it.name}") }
+ }.ifEmpty { "Unknown Location" }
+}
+
+private fun getScreenTitle(route: String?): String {
+ return when (route) {
+ AppScreen.Dashboard.route -> "Dashboard"
+ AppScreen.Locations.route -> "Locations"
+ AppScreen.Search.route -> "Search"
+ AppScreen.Overview.route -> "Overview"
+ AppScreen.Images.route -> "Images"
+ AppScreen.History.route -> "History"
+ AppScreen.Settings.route -> "Settings"
+ AppScreen.Sync.route -> "Sync"
+ AppScreen.Help.route -> "Help"
+ AppScreen.CreateItem.route -> "Create Item"
+ AppScreen.ImageEdit.route -> "Edit Image"
+ AppScreen.AIProcessing.route -> "AI Processing"
+ AppScreen.AIResults.route -> "AI Results"
+ else -> "Inventory Manager"
+ }
+}
+
+fun createImageUri(context: Context): Uri {
+ val imageFile = File(
+ context.cacheDir,
+ "captured_image_${System.currentTimeMillis()}.jpg"
+ )
+ return FileProvider.getUriForFile(
+ context,
+ "${context.packageName}.provider",
+ imageFile
+ )
+}
+
+fun saveBitmapToUri(context: Context, bitmap: Bitmap): Uri {
+ val file = File(
+ context.cacheDir,
+ "edited_image_${System.currentTimeMillis()}.jpg"
+ )
+ file.outputStream().use { out ->
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
+ }
+ return FileProvider.getUriForFile(
+ context,
+ "${context.packageName}.provider",
+ file
+ )
+}
+
@Composable
private fun HandleDialogs(
dialogState: DialogState,
garages: MutableList,
- onClearHistory: () -> Unit,
- onDismiss: () -> Unit
+ onDismiss: () -> Unit,
+ onClearHistory: () -> Unit
) {
- when (val state = dialogState) {
- is DialogState.Closed -> {}
- DialogState.AddGarage -> AddLocationDialog("Create New Garage", onDismiss) { name ->
- garages.add(Garage("g_${System.currentTimeMillis()}", name, emptyList()))
- }
- is DialogState.AddCabinet -> AddLocationDialog("Add Cabinet", onDismiss) { name ->
- val i = garages.indexOfFirst { it.id == state.garageId }
- if (i != -1) {
- val g = garages[i]
- garages[i] = g.copy(
- cabinets = g.cabinets + Cabinet("c_${System.currentTimeMillis()}", name, emptyList())
+ when (dialogState) {
+ is DialogState.AddGarage -> {
+ BasicInputDialog(title = "Add Garage", onConfirm = { name ->
+ garages.add(
+ Garage(
+ id = "garage_${System.currentTimeMillis()}",
+ name = name,
+ cabinets = mutableListOf()
+ )
)
- }
+ onDismiss()
+ }, onDismiss = onDismiss)
}
- is DialogState.AddShelf -> AddLocationDialog("Add Shelf", onDismiss) { name ->
- for ((gi, g) in garages.withIndex()) {
- val ci = g.cabinets.indexOfFirst { it.id == state.cabinetId }
- if (ci != -1) {
- val c = g.cabinets[ci]
- val newShelf = Shelf("s_${System.currentTimeMillis()}", name, emptyList())
- garages[gi] = g.copy(
- cabinets = g.cabinets.toMutableList().also { cabinetList ->
- cabinetList[ci] = c.copy(shelves = c.shelves + newShelf)
- }
+ is DialogState.AddCabinet -> {
+ BasicInputDialog(title = "Add Cabinet", onConfirm = { name ->
+ val garageIndex = garages.indexOfFirst { it.id == dialogState.garageId }
+ if (garageIndex != -1) {
+ val garage = garages[garageIndex]
+ val updatedCabinets = garage.cabinets.toMutableList()
+ updatedCabinets.add(
+ Cabinet(
+ id = "cabinet_${System.currentTimeMillis()}",
+ name = name,
+ shelves = mutableListOf()
+ )
)
- break
+ garages[garageIndex] = garage.copy(cabinets = updatedCabinets)
}
- }
+ onDismiss()
+ }, onDismiss = onDismiss)
}
- is DialogState.AddBox -> AddLocationDialog("Add Box/Bin", onDismiss) { name ->
- run loop@{
- for ((gi, g) in garages.withIndex()) {
- for ((ci, c) in g.cabinets.withIndex()) {
- val si = c.shelves.indexOfFirst { it.id == state.shelfId }
- if (si != -1) {
- val s = c.shelves[si]
- val newBox = Box("b_${System.currentTimeMillis()}", name)
- garages[gi] = g.copy(
- cabinets = g.cabinets.toMutableList().also { cabinetList ->
- cabinetList[ci] = c.copy(
- shelves = c.shelves.toMutableList().also { shelfList ->
- shelfList[si] = s.copy(boxes = s.boxes + newBox)
- }
- )
- }
+ is DialogState.AddShelf -> {
+ BasicInputDialog(title = "Add Shelf", onConfirm = { name ->
+ garages.forEachIndexed { garageIndex, garage ->
+ val cabinetIndex = garage.cabinets.indexOfFirst { it.id == dialogState.cabinetId }
+ if (cabinetIndex != -1) {
+ val cabinet = garage.cabinets[cabinetIndex]
+ val updatedShelves = cabinet.shelves.toMutableList()
+ updatedShelves.add(
+ Shelf(
+ id = "shelf_${System.currentTimeMillis()}",
+ name = name,
+ boxes = mutableListOf()
+ )
+ )
+ val updatedCabinets = garage.cabinets.toMutableList()
+ updatedCabinets[cabinetIndex] = cabinet.copy(shelves = updatedShelves)
+ garages[garageIndex] = garage.copy(cabinets = updatedCabinets)
+ }
+ }
+ onDismiss()
+ }, onDismiss = onDismiss)
+ }
+ is DialogState.AddBox -> {
+ BasicInputDialog(title = "Add Box", onConfirm = { name ->
+ garages.forEachIndexed { garageIndex, garage ->
+ garage.cabinets.forEachIndexed { cabinetIndex, cabinet ->
+ val shelfIndex = cabinet.shelves.indexOfFirst { it.id == dialogState.shelfId }
+ if (shelfIndex != -1) {
+ val shelf = cabinet.shelves[shelfIndex]
+ val updatedBoxes = shelf.boxes.toMutableList()
+ updatedBoxes.add(
+ Box(
+ id = "box_${System.currentTimeMillis()}",
+ name = name
+ )
)
- return@loop
+ val updatedShelves = cabinet.shelves.toMutableList()
+ updatedShelves[shelfIndex] = shelf.copy(boxes = updatedBoxes)
+ val updatedCabinets = garage.cabinets.toMutableList()
+ updatedCabinets[cabinetIndex] = cabinet.copy(shelves = updatedShelves)
+ garages[garageIndex] = garage.copy(cabinets = updatedCabinets)
}
}
}
- }
+ onDismiss()
+ }, onDismiss = onDismiss)
}
is DialogState.RenameLocation -> {
- RenameDialog(state, onDismiss) { /* Rename logic */ }
+ BasicInputDialog(
+ title = "Rename ${dialogState.type}",
+ initialValue = dialogState.oldName,
+ onConfirm = { newName ->
+ when (dialogState.type) {
+ "Garage" -> {
+ val index = garages.indexOfFirst { it.id == dialogState.id }
+ if (index != -1) {
+ garages[index] = garages[index].copy(name = newName)
+ }
+ }
+ "Cabinet" -> {
+ garages.forEachIndexed { garageIndex, garage ->
+ val cabinetIndex = garage.cabinets.indexOfFirst { it.id == dialogState.id }
+ if (cabinetIndex != -1) {
+ val updatedCabinets = garage.cabinets.toMutableList()
+ updatedCabinets[cabinetIndex] = updatedCabinets[cabinetIndex].copy(name = newName)
+ garages[garageIndex] = garage.copy(cabinets = updatedCabinets)
+ }
+ }
+ }
+ "Shelf" -> {
+ garages.forEachIndexed { garageIndex, garage ->
+ garage.cabinets.forEachIndexed { cabinetIndex, cabinet ->
+ val shelfIndex = cabinet.shelves.indexOfFirst { it.id == dialogState.id }
+ if (shelfIndex != -1) {
+ val updatedShelves = cabinet.shelves.toMutableList()
+ updatedShelves[shelfIndex] = updatedShelves[shelfIndex].copy(name = newName)
+ val updatedCabinets = garage.cabinets.toMutableList()
+ updatedCabinets[cabinetIndex] = cabinet.copy(shelves = updatedShelves)
+ garages[garageIndex] = garage.copy(cabinets = updatedCabinets)
+ }
+ }
+ }
+ }
+ "Box" -> {
+ garages.forEachIndexed { garageIndex, garage ->
+ garage.cabinets.forEachIndexed { cabinetIndex, cabinet ->
+ cabinet.shelves.forEachIndexed { shelfIndex, shelf ->
+ val boxIndex = shelf.boxes.indexOfFirst { it.id == dialogState.id }
+ if (boxIndex != -1) {
+ val updatedBoxes = shelf.boxes.toMutableList()
+ updatedBoxes[boxIndex] = updatedBoxes[boxIndex].copy(name = newName)
+ val updatedShelves = cabinet.shelves.toMutableList()
+ updatedShelves[shelfIndex] = shelf.copy(boxes = updatedBoxes)
+ val updatedCabinets = garage.cabinets.toMutableList()
+ updatedCabinets[cabinetIndex] = cabinet.copy(shelves = updatedShelves)
+ garages[garageIndex] = garage.copy(cabinets = updatedCabinets)
+ }
+ }
+ }
+ }
+ }
+ }
+ onDismiss()
+ },
+ onDismiss = onDismiss
+ )
}
is DialogState.DeleteLocation -> {
- DeleteConfirmDialog(state, onDismiss) { /* Delete logic */ }
- }
- is DialogState.ClearHistory -> {
AlertDialog(
onDismissRequest = onDismiss,
- title = { Text("Clear History?") },
- text = { Text("Are you sure you want to clear all history? This cannot be undone.") },
+ title = { Text("Delete ${dialogState.type}?") },
+ text = { Text("Are you sure you want to delete '${dialogState.name}'? This cannot be undone.") },
confirmButton = {
- Button(
- onClick = onClearHistory,
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.error
- )
- ) {
- Text("Clear All")
- }
+ Button(onClick = {
+ when (dialogState.type) {
+ "Garage" -> {
+ garages.removeIf { it.id == dialogState.id }
+ }
+ "Cabinet" -> {
+ garages.forEachIndexed { garageIndex, garage ->
+ val updatedCabinets = garage.cabinets.filter { it.id != dialogState.id }
+ if (updatedCabinets.size != garage.cabinets.size) {
+ garages[garageIndex] = garage.copy(cabinets = updatedCabinets as MutableList)
+ }
+ }
+ }
+ "Shelf" -> {
+ garages.forEachIndexed { garageIndex, garage ->
+ garage.cabinets.forEachIndexed { cabinetIndex, cabinet ->
+ val updatedShelves = cabinet.shelves.filter { it.id != dialogState.id }
+ if (updatedShelves.size != cabinet.shelves.size) {
+ val updatedCabinets = garage.cabinets.toMutableList()
+ updatedCabinets[cabinetIndex] = cabinet.copy(shelves = updatedShelves as MutableList)
+ garages[garageIndex] = garage.copy(cabinets = updatedCabinets)
+ }
+ }
+ }
+ }
+ "Box" -> {
+ garages.forEachIndexed { garageIndex, garage ->
+ garage.cabinets.forEachIndexed { cabinetIndex, cabinet ->
+ cabinet.shelves.forEachIndexed { shelfIndex, shelf ->
+ val updatedBoxes = shelf.boxes.filter { it.id != dialogState.id }
+ if (updatedBoxes.size != shelf.boxes.size) {
+ val updatedShelves = cabinet.shelves.toMutableList()
+ updatedShelves[shelfIndex] = shelf.copy(boxes = updatedBoxes as MutableList)
+ val updatedCabinets = garage.cabinets.toMutableList()
+ updatedCabinets[cabinetIndex] = cabinet.copy(shelves = updatedShelves)
+ garages[garageIndex] = garage.copy(cabinets = updatedCabinets)
+ }
+ }
+ }
+ }
+ }
+ }
+ onDismiss()
+ }) { Text("Delete") }
},
dismissButton = {
- TextButton(onClick = onDismiss) {
- Text("Cancel")
- }
+ TextButton(onClick = onDismiss) { Text("Cancel") }
}
)
}
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun AddLocationDialog(title: String, onDismiss: () -> Unit, onConfirm: (String) -> Unit) {
- var text by remember { mutableStateOf("") }
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text(title) },
- text = {
- OutlinedTextField(
- value = text,
- onValueChange = { newText -> text = newText },
- label = { Text("Location Name") }
- )
- },
- confirmButton = {
- Button(
- onClick = {
- if (text.isNotBlank()) onConfirm(text)
- onDismiss()
- }
- ) {
- Text("Add")
- }
- },
- dismissButton = {
- TextButton(onClick = onDismiss) {
- Text("Cancel")
- }
- }
- )
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun RenameDialog(
- state: DialogState.RenameLocation,
- onDismiss: () -> Unit,
- onConfirm: (String) -> Unit
-) {
- var text by remember { mutableStateOf(state.oldName) }
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text("Rename ${state.type.replaceFirstChar { it.uppercase() }}") },
- text = {
- OutlinedTextField(
- value = text,
- onValueChange = { newText -> text = newText },
- label = { Text("New Name") }
+ is DialogState.ClearHistory -> {
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text("Clear History") },
+ text = { Text("Clear all history logs?") },
+ confirmButton = { Button(onClick = onClearHistory) { Text("Clear") } },
+ dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } }
)
- },
- confirmButton = {
- Button(
- onClick = {
- if (text.isNotBlank()) onConfirm(text)
- onDismiss()
- }
- ) {
- Text("Rename")
- }
- },
- dismissButton = {
- TextButton(onDismiss) {
- Text("Cancel")
- }
}
- )
+ DialogState.Closed -> { /* No dialog */ }
+ }
}
@Composable
-private fun DeleteConfirmDialog(
- state: DialogState.DeleteLocation,
- onDismiss: () -> Unit,
- onConfirm: () -> Unit
+fun BasicInputDialog(
+ title: String,
+ initialValue: String = "",
+ onConfirm: (String) -> Unit,
+ onDismiss: () -> Unit
) {
+ var text by remember { mutableStateOf(initialValue) }
AlertDialog(
onDismissRequest = onDismiss,
- title = { Text("Delete ${state.type.replaceFirstChar { it.uppercase() }}?") },
- text = {
- Text("Are you sure you want to delete '${state.name}'? This cannot be undone.")
- },
- confirmButton = {
- Button(
- onClick = {
- onConfirm()
- onDismiss()
- },
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.error
- )
- ) {
- Text("Delete")
- }
- },
- dismissButton = {
- TextButton(onDismiss) {
- Text("Cancel")
- }
- }
+ title = { Text(title) },
+ text = { OutlinedTextField(value = text, onValueChange = { text = it }) },
+ confirmButton = { Button(onClick = { onConfirm(text) }) { Text("OK") } },
+ dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } }
)
}
-// ========================================================================================
-// DATA PERSISTENCE FUNCTIONS
-// ========================================================================================
-
-private fun saveDataToFile(context: Context, appData: AppData) {
- try {
- context.openFileOutput("inventory.json", Context.MODE_PRIVATE).use {
- it.write(Gson().toJson(appData).toByteArray())
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
-}
-
private fun loadDataFromFile(
context: Context,
garages: MutableList,
items: MutableList
- ,
history: MutableList
) {
- val file = File(context.filesDir, "inventory.json")
- if (!file.exists()) return
try {
- context.openFileInput("inventory.json").use { stream ->
- InputStreamReader(stream).use { reader ->
- val data = Gson().fromJson(reader, AppData::class.java)
- garages.clear()
- items.clear()
- history.clear()
- garages.addAll(data.garages)
- items.addAll(data.items)
- history.addAll(data.history)
- }
+ val file = File(context.filesDir, "app_data.json")
+ if (file.exists()) {
+ val json = file.readText()
+ val data = Gson().fromJson(json, AppData::class.java)
+ garages.clear()
+ garages.addAll(data.garages)
+ items.clear()
+ items.addAll(data.items)
+ history.clear()
+ history.addAll(data.history)
}
- } catch (_: Exception) {
- // Failed to load data, starting fresh
+ } catch (e: Exception) {
+ e.printStackTrace()
}
}
-private fun saveSettingsToFile(context: Context, settings: AppSettings) {
+private fun saveDataToFile(context: Context, data: AppData) {
try {
- context.openFileOutput("app_settings.json", Context.MODE_PRIVATE).use {
- it.write(Gson().toJson(settings).toByteArray())
- }
+ val file = File(context.filesDir, "app_data.json")
+ val json = Gson().toJson(data)
+ file.writeText(json)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun loadSettingsFromFile(context: Context): AppSettings {
- val file = File(context.filesDir, "app_settings.json")
- return if (file.exists()) {
- try {
- Gson().fromJson(file.readText(), AppSettings::class.java)
- } catch (e: Exception) {
+ return try {
+ val file = File(context.filesDir, "settings.json")
+ if (file.exists()) {
+ val reader = InputStreamReader(file.inputStream())
+ Gson().fromJson(reader, AppSettings::class.java)
+ } else {
AppSettings()
}
- } else {
+ } catch (e: Exception) {
+ e.printStackTrace()
AppSettings()
}
+}
+
+private fun saveSettingsToFile(context: Context, settings: AppSettings) {
+ try {
+ val file = File(context.filesDir, "settings.json")
+ val json = Gson().toJson(settings)
+ file.writeText(json)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/OnboardingScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/OnboardingScreen.kt
new file mode 100644
index 0000000..b1c51b7
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/OnboardingScreen.kt
@@ -0,0 +1,493 @@
+package com.samuel.inventorymanager.screens
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.EaseInOut
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.slideInVertically
+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.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Backup
+import androidx.compose.material.icons.filled.CameraAlt
+import androidx.compose.material.icons.filled.Cloud
+import androidx.compose.material.icons.filled.Dashboard
+import androidx.compose.material.icons.filled.FilterAlt
+import androidx.compose.material.icons.filled.Inventory
+import androidx.compose.material.icons.filled.Inventory2
+import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.material.icons.filled.Psychology
+import androidx.compose.material.icons.filled.QrCode
+import androidx.compose.material.icons.filled.Search
+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.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+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.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.google.accompanist.pager.ExperimentalPagerApi
+import com.google.accompanist.pager.HorizontalPager
+import com.google.accompanist.pager.HorizontalPagerIndicator
+import com.google.accompanist.pager.rememberPagerState
+import kotlinx.coroutines.delay
+
+@OptIn(ExperimentalPagerApi::class)
+@Composable
+fun OnboardingScreen(
+ onGetStarted: () -> Unit,
+ onSignInWithGoogle: () -> Unit
+) {
+ var currentPage by remember { mutableStateOf(0) }
+ val pagerState = rememberPagerState()
+ val scope = rememberCoroutineScope()
+
+ // Auto-advance pages
+ LaunchedEffect(Unit) {
+ while (true) {
+ delay(5000) // 5 seconds per page
+ val nextPage = (pagerState.currentPage + 1) % 5
+ pagerState.animateScrollToPage(nextPage)
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ Brush.verticalGradient(
+ colors = listOf(
+ Color(0xFF1A237E),
+ Color(0xFF0D47A1),
+ Color(0xFF01579B)
+ )
+ )
+ )
+ ) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // Top App Logo & Title
+ AnimatedAppHeader()
+
+ // Horizontal Pager for feature showcase
+ HorizontalPager(
+ count = 5,
+ state = pagerState,
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth()
+ ) { page ->
+ OnboardingPage(
+ page = when (page) {
+ 0 -> OnboardingPageData(
+ icon = Icons.Default.Inventory,
+ title = "Welcome to Android Inventory Pro",
+ description = "The ultimate solution for organizing everything you own. From your garage tools to your kitchen supplies, never lose track of anything again!",
+ emoji = "📦"
+ )
+ 1 -> OnboardingPageData(
+ icon = Icons.Default.LocationOn,
+ title = "Smart Location Hierarchy",
+ description = "Create Garages → Cabinets → Shelves → Boxes. Know exactly where every item is stored with our intuitive 4-level organization system.",
+ emoji = "🏠"
+ )
+ 2 -> OnboardingPageData(
+ icon = Icons.Default.CameraAlt,
+ title = "AI-Powered Recognition",
+ description = "Take a photo and let our AI do the work! Automatically detect item names, model numbers, and descriptions. Save hours of manual data entry.",
+ emoji = "🤖"
+ )
+ 3 -> OnboardingPageData(
+ icon = Icons.Default.Search,
+ title = "Instant Search & Filters",
+ description = "Find any item in seconds with powerful search. Filter by location, condition, tags, and more. Your entire inventory at your fingertips!",
+ emoji = "🔍"
+ )
+ else -> OnboardingPageData(
+ icon = Icons.Default.Cloud,
+ title = "Secure Cloud Backup",
+ description = "Never lose your data! Sign in with Google to automatically backup your inventory to Google Drive. Sync across all your devices seamlessly.",
+ emoji = "☁️"
+ )
+ }
+ )
+ }
+
+ // Page Indicators
+ HorizontalPagerIndicator(
+ pagerState = pagerState,
+ modifier = Modifier.padding(16.dp),
+ activeColor = Color.White,
+ inactiveColor = Color.White.copy(alpha = 0.3f)
+ )
+
+ // Bottom Action Section
+ BottomActionSection(
+ onSignInWithGoogle = onSignInWithGoogle,
+ onContinueWithoutSignIn = onGetStarted
+ )
+ }
+ }
+}
+
+@Composable
+private fun AnimatedAppHeader() {
+ val infiniteTransition = rememberInfiniteTransition(label = "header")
+ val scale by infiniteTransition.animateFloat(
+ initialValue = 1f,
+ targetValue = 1.1f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(2000, easing = EaseInOut),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "scale"
+ )
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 48.dp, bottom = 24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // App Icon Placeholder (you can replace with actual drawable)
+ Surface(
+ modifier = Modifier
+ .size(100.dp)
+ .scale(scale),
+ shape = RoundedCornerShape(24.dp),
+ color = Color.White.copy(alpha = 0.2f)
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ "📦",
+ fontSize = 48.sp
+ )
+ }
+ }
+
+ Spacer(Modifier.height(16.dp))
+
+ Text(
+ "Android Inventory Pro",
+ fontSize = 28.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color.White,
+ textAlign = TextAlign.Center
+ )
+
+ Text(
+ "Organize Everything, Find Anything",
+ fontSize = 16.sp,
+ color = Color.White.copy(alpha = 0.8f),
+ textAlign = TextAlign.Center
+ )
+ }
+}
+
+data class OnboardingPageData(
+ val icon: ImageVector,
+ val title: String,
+ val description: String,
+ val emoji: String
+)
+
+@Composable
+private fun OnboardingPage(page: OnboardingPageData) {
+ var visible by remember { mutableStateOf(false) }
+
+ LaunchedEffect(Unit) {
+ visible = true
+ }
+
+ AnimatedVisibility(
+ visible = visible,
+ enter = fadeIn(tween(800)) + slideInVertically(tween(800)) { it / 2 }
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(24.dp)
+ .verticalScroll(rememberScrollState()),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ // Large Emoji
+ Text(
+ page.emoji,
+ fontSize = 80.sp,
+ modifier = Modifier.padding(bottom = 24.dp)
+ )
+
+ // Icon
+ Surface(
+ modifier = Modifier
+ .size(80.dp)
+ .padding(bottom = 24.dp),
+ shape = CircleShape,
+ color = Color.White.copy(alpha = 0.2f)
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ page.icon,
+ contentDescription = null,
+ modifier = Modifier.size(40.dp),
+ tint = Color.White
+ )
+ }
+ }
+
+ // Title
+ Text(
+ page.title,
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color.White,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+
+ // Description
+ Text(
+ page.description,
+ fontSize = 16.sp,
+ color = Color.White.copy(alpha = 0.9f),
+ textAlign = TextAlign.Center,
+ lineHeight = 24.sp
+ )
+ }
+ }
+}
+
+@Composable
+private fun BottomActionSection(
+ onSignInWithGoogle: () -> Unit,
+ onContinueWithoutSignIn: () -> Unit
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ Color.White.copy(alpha = 0.1f),
+ RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp)
+ )
+ .padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ // Sign in with Google Button
+ Button(
+ onClick = onSignInWithGoogle,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp),
+ shape = RoundedCornerShape(16.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color.White,
+ contentColor = Color(0xFF1A237E)
+ )
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ // Google Icon (simplified)
+ Surface(
+ modifier = Modifier.size(24.dp),
+ shape = CircleShape,
+ color = Color.Transparent
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text("G", fontSize = 18.sp, fontWeight = FontWeight.Bold)
+ }
+ }
+ Spacer(Modifier.width(12.dp))
+ Text(
+ "Sign in with Google",
+ fontSize = 16.sp,
+ fontWeight = FontWeight.SemiBold
+ )
+ }
+ }
+
+ // Continue without sign in
+ TextButton(
+ onClick = onContinueWithoutSignIn,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(
+ "Continue without signing in",
+ color = Color.White,
+ fontSize = 14.sp
+ )
+ }
+
+ // Benefits text
+ Text(
+ "Sign in to enable cloud backup and sync",
+ fontSize = 12.sp,
+ color = Color.White.copy(alpha = 0.7f),
+ textAlign = TextAlign.Center
+ )
+ }
+}
+
+// ==========================================
+// Feature Highlights Composable (Alternative detailed view)
+// ==========================================
+
+@Composable
+fun FeatureHighlightsSection() {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(24.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ "Key Features",
+ fontSize = 22.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color.White,
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+
+ FeatureCard(
+ icon = Icons.Default.Inventory2,
+ title = "4-Level Organization",
+ description = "Garage → Cabinet → Shelf → Box hierarchy"
+ )
+
+ FeatureCard(
+ icon = Icons.Default.Psychology,
+ title = "AI Recognition",
+ description = "Auto-detect items from photos with ML Kit"
+ )
+
+ FeatureCard(
+ icon = Icons.Default.QrCode,
+ title = "Barcode & OCR",
+ description = "Scan barcodes and extract text from images"
+ )
+
+ FeatureCard(
+ icon = Icons.Default.Backup,
+ title = "Google Drive Backup",
+ description = "Automatic cloud backup with your Google account"
+ )
+
+ FeatureCard(
+ icon = Icons.Default.Dashboard,
+ title = "Overview Dashboard",
+ description = "Complete spreadsheet view of all items"
+ )
+
+ FeatureCard(
+ icon = Icons.Default.FilterAlt,
+ title = "Advanced Filters",
+ description = "Filter by location, condition, tags, and more"
+ )
+ }
+}
+
+@Composable
+private fun FeatureCard(
+ icon: ImageVector,
+ title: String,
+ description: String
+) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Color.White.copy(alpha = 0.15f)
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Surface(
+ modifier = Modifier.size(48.dp),
+ shape = CircleShape,
+ color = Color.White.copy(alpha = 0.2f)
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ icon,
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ }
+
+ Spacer(Modifier.width(16.dp))
+
+ Column {
+ Text(
+ title,
+ fontSize = 16.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = Color.White
+ )
+ Text(
+ description,
+ fontSize = 13.sp,
+ color = Color.White.copy(alpha = 0.8f)
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/OverviewScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/OverviewScreen.kt
index 2037b9c..eacded6 100644
--- a/app/src/main/java/com/samuel/inventorymanager/screens/OverviewScreen.kt
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/OverviewScreen.kt
@@ -1,11 +1,5 @@
package com.samuel.inventorymanager.screens
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.expandVertically
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.shrinkVertically
-import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -21,31 +15,30 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.ArrowDownward
+import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.AttachMoney
-import androidx.compose.material.icons.filled.Category
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Clear
-import androidx.compose.material.icons.filled.ExpandLess
-import androidx.compose.material.icons.filled.ExpandMore
-import androidx.compose.material.icons.filled.FileDownload
+import androidx.compose.material.icons.filled.HomeWork
import androidx.compose.material.icons.filled.Inventory
import androidx.compose.material.icons.filled.List
-import androidx.compose.material.icons.filled.LocationCity
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Numbers
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Scale
import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Sort
import androidx.compose.material.icons.filled.Straighten
import androidx.compose.material.icons.filled.TrendingUp
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.Divider
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -54,8 +47,6 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Tab
-import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -64,9 +55,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
@@ -75,9 +63,9 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-// ======================================================================
-// MAIN OVERVIEW SCREEN - REDESIGNED
-// ======================================================================
+enum class SortType {
+ NAME_ASC, NAME_DESC, PRICE_ASC, PRICE_DESC, DATE_NEW, DATE_OLD
+}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -86,28 +74,43 @@ fun OverviewScreen(
garages: List
) {
var searchQuery by remember { mutableStateOf("") }
- var selectedLocation by remember { mutableStateOf("All Locations") }
+ var selectedGarageId by remember { mutableStateOf(null) }
var selectedSize by remember { mutableStateOf("All Sizes") }
var selectedCondition by remember { mutableStateOf("All Conditions") }
- var showLocationMenu by remember { mutableStateOf(false) }
var showSizeMenu by remember { mutableStateOf(false) }
var showConditionMenu by remember { mutableStateOf(false) }
- var selectedTab by remember { mutableStateOf(0) }
- var expandedStats by remember { mutableStateOf(true) }
- var expandedAnalytics by remember { mutableStateOf(true) }
+ var sortType by remember { mutableStateOf(SortType.NAME_ASC) }
+ var showSortMenu by remember { mutableStateOf(false) }
+ var selectedItemForDetail by remember { mutableStateOf
- (null) }
+
+ // Filter by garage if selected
+ val garageFilteredItems = if (selectedGarageId != null) {
+ items.filter { it.garageId == selectedGarageId }
+ } else {
+ items
+ }
- val filteredItems = remember(searchQuery, selectedLocation, selectedSize, selectedCondition, items) {
- items.filter { item ->
+ // Apply other filters
+ val filteredItems = remember(searchQuery, garageFilteredItems, selectedSize, selectedCondition, sortType) {
+ var result = garageFilteredItems.filter { item ->
val matchesSearch = searchQuery.isBlank() ||
item.name.contains(searchQuery, ignoreCase = true) ||
item.modelNumber?.contains(searchQuery, ignoreCase = true) == true
- val matchesLocation = selectedLocation == "All Locations" ||
- (garages.find { it.id == item.garageId }?.name == selectedLocation)
val matchesSize = selectedSize == "All Sizes" ||
item.sizeCategory.equals(selectedSize, ignoreCase = true)
val matchesCondition = selectedCondition == "All Conditions" ||
item.condition.equals(selectedCondition, ignoreCase = true)
- matchesSearch && matchesLocation && matchesSize && matchesCondition
+ matchesSearch && matchesSize && matchesCondition
+ }
+
+ // Apply sorting
+ when (sortType) {
+ SortType.NAME_ASC -> result.sortedBy { it.name.lowercase() }
+ SortType.NAME_DESC -> result.sortedByDescending { it.name.lowercase() }
+ SortType.PRICE_ASC -> result.sortedBy { ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 }
+ SortType.PRICE_DESC -> result.sortedByDescending { ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 }
+ SortType.DATE_NEW -> result.reversed() // Newest first (assuming items list is chronological)
+ SortType.DATE_OLD -> result // Oldest first
}
}
@@ -120,6 +123,16 @@ fun OverviewScreen(
val conditions = items.map { it.condition }.distinct().sorted()
+ // Show item detail PDF if selected
+ if (selectedItemForDetail != null) {
+ ItemDetailPDFScreen(
+ item = selectedItemForDetail!!,
+ garage = garages.find { it.id == selectedItemForDetail!!.garageId },
+ onBackClick = { selectedItemForDetail = null }
+ )
+ return
+ }
+
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
@@ -129,7 +142,7 @@ fun OverviewScreen(
item {
Row(
verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(vertical = 4.dp)
+ modifier = Modifier.padding(vertical = 8.dp)
) {
Icon(
Icons.Default.List,
@@ -144,9 +157,6 @@ fun OverviewScreen(
fontWeight = FontWeight.Bold
)
Spacer(Modifier.weight(1f))
- IconButton(onClick = { /* TODO: Export */ }) {
- Icon(Icons.Default.FileDownload, "Export", tint = MaterialTheme.colorScheme.primary)
- }
IconButton(onClick = { /* TODO: Refresh */ }) {
Icon(Icons.Default.Refresh, "Refresh", tint = MaterialTheme.colorScheme.primary)
}
@@ -159,7 +169,7 @@ fun OverviewScreen(
value = searchQuery,
onValueChange = { searchQuery = it },
modifier = Modifier.fillMaxWidth(),
- placeholder = { Text("Search by name or model number...") },
+ placeholder = { Text("Search items...", fontSize = 14.sp) },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
trailingIcon = {
if (searchQuery.isNotEmpty()) {
@@ -168,195 +178,258 @@ fun OverviewScreen(
}
}
},
- shape = RoundedCornerShape(16.dp),
- singleLine = true
+ shape = RoundedCornerShape(12.dp),
+ singleLine = true,
+ textStyle = MaterialTheme.typography.bodyMedium
)
}
// Filter Row
item {
- Row(
- horizontalArrangement = Arrangement.spacedBy(12.dp),
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
- FilterDropdown(
- label = selectedLocation,
- icon = Icons.Default.LocationOn,
- expanded = showLocationMenu,
- onExpandChange = { showLocationMenu = it },
- options = listOf("All Locations") + garages.map { it.name },
- onSelect = { selectedLocation = it; showLocationMenu = false },
- modifier = Modifier.weight(1f)
- )
-
- FilterDropdown(
- label = selectedSize,
- icon = Icons.Default.Straighten,
- expanded = showSizeMenu,
- onExpandChange = { showSizeMenu = it },
- options = listOf("All Sizes", "Small", "Medium", "Large"),
- onSelect = { selectedSize = it; showSizeMenu = false },
- modifier = Modifier.weight(1f)
+ Text(
+ "Filters",
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.Bold
)
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ FilterDropdownCompact(
+ label = selectedSize,
+ icon = Icons.Default.Straighten,
+ expanded = showSizeMenu,
+ onExpandChange = { showSizeMenu = it },
+ options = listOf("All Sizes", "Small", "Medium", "Large"),
+ onSelect = { selectedSize = it; showSizeMenu = false },
+ modifier = Modifier.weight(1f)
+ )
- FilterDropdown(
- label = selectedCondition,
- icon = Icons.Default.CheckCircle,
- expanded = showConditionMenu,
- onExpandChange = { showConditionMenu = it },
- options = listOf("All Conditions") + conditions,
- onSelect = { selectedCondition = it; showConditionMenu = false },
- modifier = Modifier.weight(1f)
- )
+ FilterDropdownCompact(
+ label = selectedCondition,
+ icon = Icons.Default.CheckCircle,
+ expanded = showConditionMenu,
+ onExpandChange = { showConditionMenu = it },
+ options = listOf("All Conditions") + conditions,
+ onSelect = { selectedCondition = it; showConditionMenu = false },
+ modifier = Modifier.weight(1f)
+ )
+ }
}
}
- // Statistics Section
+ // Sort Button
item {
- ExpandableSection(
- title = "Key Statistics",
- subtitle = "${filteredItems.size} items",
- icon = Icons.Default.TrendingUp,
- isExpanded = expandedStats,
- onToggle = { expandedStats = !expandedStats }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
) {
- Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
- Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
- ModernStatCard(
- title = "Unique Items",
- value = totalItemsCount.toString(),
- icon = Icons.Default.Inventory,
- color = Color(0xFF6200EE),
- modifier = Modifier.weight(1f)
- )
- ModernStatCard(
- title = "Total Quantity",
- value = totalQuantity.toString(),
- icon = Icons.Default.Numbers,
- color = Color(0xFF03DAC5),
- modifier = Modifier.weight(1f)
+ Text(
+ "Sort by:",
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Box {
+ OutlinedButton(
+ onClick = { showSortMenu = true },
+ shape = RoundedCornerShape(8.dp)
+ ) {
+ Icon(Icons.Default.Sort, null, modifier = Modifier.size(16.dp))
+ Spacer(Modifier.width(4.dp))
+ Text(
+ when (sortType) {
+ SortType.NAME_ASC -> "Name (A-Z)"
+ SortType.NAME_DESC -> "Name (Z-A)"
+ SortType.PRICE_ASC -> "Price (Low-High)"
+ SortType.PRICE_DESC -> "Price (High-Low)"
+ SortType.DATE_NEW -> "Newest First"
+ SortType.DATE_OLD -> "Oldest First"
+ },
+ fontSize = 12.sp
)
}
- Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
- ModernStatCard(
- title = "Total Value",
- value = "$${String.format("%.0f", totalValue)}",
- icon = Icons.Default.AttachMoney,
- color = Color(0xFF047857),
- modifier = Modifier.weight(1f)
+ DropdownMenu(
+ expanded = showSortMenu,
+ onDismissRequest = { showSortMenu = false }
+ ) {
+ DropdownMenuItem(
+ text = { Text("Name (A-Z)") },
+ onClick = { sortType = SortType.NAME_ASC; showSortMenu = false },
+ leadingIcon = { Icon(Icons.Default.ArrowUpward, null) }
)
- ModernStatCard(
- title = "Avg Value",
- value = "$${String.format("%.0f", avgItemValue)}",
- icon = Icons.Default.TrendingUp,
- color = Color(0xFFB45309),
- modifier = Modifier.weight(1f)
+ DropdownMenuItem(
+ text = { Text("Name (Z-A)") },
+ onClick = { sortType = SortType.NAME_DESC; showSortMenu = false },
+ leadingIcon = { Icon(Icons.Default.ArrowDownward, null) }
)
- }
- Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
- ModernStatCard(
- title = "Total Weight",
- value = "${String.format("%.1f", totalWeight)} lbs",
- icon = Icons.Default.Scale,
- color = Color(0xFF0369A1),
- modifier = Modifier.weight(1f)
+ DropdownMenuItem(
+ text = { Text("Price (Low-High)") },
+ onClick = { sortType = SortType.PRICE_ASC; showSortMenu = false },
+ leadingIcon = { Icon(Icons.Default.ArrowUpward, null) }
+ )
+ DropdownMenuItem(
+ text = { Text("Price (High-Low)") },
+ onClick = { sortType = SortType.PRICE_DESC; showSortMenu = false },
+ leadingIcon = { Icon(Icons.Default.ArrowDownward, null) }
+ )
+ DropdownMenuItem(
+ text = { Text("Newest First") },
+ onClick = { sortType = SortType.DATE_NEW; showSortMenu = false }
)
- ModernStatCard(
- title = "Locations Used",
- value = "$locationsUsed/${garages.size}",
- icon = Icons.Default.LocationCity,
- color = Color(0xFFBE185D),
- modifier = Modifier.weight(1f)
+ DropdownMenuItem(
+ text = { Text("Oldest First") },
+ onClick = { sortType = SortType.DATE_OLD; showSortMenu = false }
)
}
}
}
}
-
- // Advanced Analytics Section
+ // Key Statistics Section
item {
- ExpandableSection(
- title = "Advanced Analytics",
- subtitle = "Visual breakdown",
- icon = Icons.Default.Category,
- isExpanded = expandedAnalytics,
- onToggle = { expandedAnalytics = !expandedAnalytics }
- ) {
- Column {
- TabRow(
- selectedTabIndex = selectedTab,
- containerColor = Color.Transparent
- ) {
- Tab(
- selected = selectedTab == 0,
- onClick = { selectedTab = 0 },
- text = { Text("Category", fontSize = 13.sp) }
- )
- Tab(
- selected = selectedTab == 1,
- onClick = { selectedTab = 1 },
- text = { Text("Condition", fontSize = 13.sp) }
- )
- Tab(
- selected = selectedTab == 2,
- onClick = { selectedTab = 2 },
- text = { Text("Location", fontSize = 13.sp) }
- )
- Tab(
- selected = selectedTab == 3,
- onClick = { selectedTab = 3 },
- text = { Text("Size", fontSize = 13.sp) }
- )
- }
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Text(
+ "Key Statistics",
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold
+ )
- Spacer(Modifier.height(20.dp))
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ CompactStatCard(
+ title = "Unique Items",
+ value = totalItemsCount.toString(),
+ icon = Icons.Default.Inventory,
+ color = Color(0xFF6200EE),
+ modifier = Modifier.weight(1f)
+ )
+ CompactStatCard(
+ title = "Total Qty",
+ value = totalQuantity.toString(),
+ icon = Icons.Default.Numbers,
+ color = Color(0xFF03DAC5),
+ modifier = Modifier.weight(1f)
+ )
+ }
- when (selectedTab) {
- 0 -> AdvancedAnalyticsView(
- groupedItems = filteredItems.groupBy { it.sizeCategory },
- label = "Category"
- )
- 1 -> AdvancedAnalyticsView(
- groupedItems = filteredItems.groupBy { it.condition },
- label = "Condition"
- )
- 2 -> {
- val locationData = garages.associate { garage ->
- garage.name to filteredItems.filter { it.garageId == garage.id }
- }.filter { it.value.isNotEmpty() }
- AdvancedAnalyticsView(locationData, "Location")
- }
- 3 -> AdvancedAnalyticsView(
- groupedItems = filteredItems.groupBy { it.sizeCategory },
- label = "Size"
- )
- }
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ CompactStatCard(
+ title = "Total Value",
+ value = "$${String.format("%.0f", totalValue)}",
+ icon = Icons.Default.AttachMoney,
+ color = Color(0xFF047857),
+ modifier = Modifier.weight(1f)
+ )
+ CompactStatCard(
+ title = "Avg Value",
+ value = "$${String.format("%.0f", avgItemValue)}",
+ icon = Icons.Default.TrendingUp,
+ color = Color(0xFFB45309),
+ modifier = Modifier.weight(1f)
+ )
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ CompactStatCard(
+ title = "Total Weight",
+ value = "${String.format("%.1f", totalWeight)} lbs",
+ icon = Icons.Default.Scale,
+ color = Color(0xFF0369A1),
+ modifier = Modifier.weight(1f)
+ )
+ CompactStatCard(
+ title = "Locations",
+ value = "$locationsUsed/${garages.size}",
+ icon = Icons.Default.LocationOn,
+ color = Color(0xFFBE185D),
+ modifier = Modifier.weight(1f)
+ )
}
}
}
- // Item List
+ // Your Garages Section
item {
Text(
- "Item List (${filteredItems.size})",
+ "Your Garages",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 8.dp)
)
}
- items(filteredItems) { item ->
- ModernItemCard(item = item, garages = garages)
+ item {
+ LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ items(garages) { garage ->
+ GarageOverviewCard(
+ garageName = garage.name,
+ itemCount = items.count { it.garageId == garage.id },
+ isSelected = selectedGarageId == garage.id,
+ onClick = {
+ selectedGarageId = if (selectedGarageId == garage.id) null else garage.id
+ }
+ )
+ }
+ }
+ }
+
+ // Item List Header
+ item {
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
+ ) {
+ Text(
+ "Items (${filteredItems.size})",
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+
+ // Item List
+ if (filteredItems.isEmpty()) {
+ item {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(32.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ "No items found",
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ } else {
+ items(filteredItems) { item ->
+ SimpleItemCard(
+ item = item,
+ garages = garages,
+ onClick = { selectedItemForDetail = item }
+ )
+ }
}
}
}
-// ======================================================================
-// MODERN COMPONENTS
-// ======================================================================
-
@Composable
-fun FilterDropdown(
+fun FilterDropdownCompact(
label: String,
icon: ImageVector,
expanded: Boolean,
@@ -369,18 +442,17 @@ fun FilterDropdown(
OutlinedButton(
onClick = { onExpandChange(true) },
modifier = Modifier.fillMaxWidth(),
- shape = RoundedCornerShape(12.dp)
+ shape = RoundedCornerShape(8.dp),
+ contentPadding = PaddingValues(8.dp)
) {
- Icon(icon, null, modifier = Modifier.size(18.dp))
- Spacer(Modifier.width(6.dp))
+ Icon(icon, null, modifier = Modifier.size(16.dp))
+ Spacer(Modifier.width(4.dp))
Text(
label,
- modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
- fontSize = 13.sp
+ fontSize = 12.sp
)
- Icon(Icons.Default.ArrowDropDown, null, modifier = Modifier.size(20.dp))
}
DropdownMenu(
expanded = expanded,
@@ -388,7 +460,7 @@ fun FilterDropdown(
) {
options.forEach { option ->
DropdownMenuItem(
- text = { Text(option) },
+ text = { Text(option, fontSize = 13.sp) },
onClick = { onSelect(option) }
)
}
@@ -397,7 +469,7 @@ fun FilterDropdown(
}
@Composable
-fun ModernStatCard(
+fun CompactStatCard(
title: String,
value: String,
icon: ImageVector,
@@ -405,34 +477,97 @@ fun ModernStatCard(
modifier: Modifier = Modifier
) {
Card(
- modifier = modifier.height(100.dp),
- shape = RoundedCornerShape(16.dp),
+ modifier = modifier.height(90.dp),
+ shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = color.copy(alpha = 0.1f)),
- elevation = CardDefaults.cardElevation(2.dp)
+ elevation = CardDefaults.cardElevation(1.dp)
) {
Column(
modifier = Modifier
- .padding(16.dp)
+ .padding(12.dp)
.fillMaxSize(),
- verticalArrangement = Arrangement.SpaceBetween
+ verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
icon,
contentDescription = null,
tint = color,
- modifier = Modifier.size(28.dp)
+ modifier = Modifier.size(20.dp)
)
- Column {
+ Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.Center) {
Text(
value,
- style = MaterialTheme.typography.headlineSmall,
+ style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
- color = color
+ color = color,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
)
Text(
title,
+ style = MaterialTheme.typography.labelSmall,
+ color = color.copy(alpha = 0.8f),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun GarageOverviewCard(
+ garageName: String,
+ itemCount: Int,
+ isSelected: Boolean,
+ onClick: () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .size(width = 140.dp, height = 100.dp)
+ .clickable(onClick = onClick),
+ colors = CardDefaults.cardColors(
+ containerColor = if (isSelected)
+ MaterialTheme.colorScheme.primary
+ else
+ MaterialTheme.colorScheme.tertiaryContainer
+ ),
+ elevation = CardDefaults.cardElevation(if (isSelected) 8.dp else 2.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(12.dp)
+ .fillMaxSize(),
+ verticalArrangement = Arrangement.SpaceBetween
+ ) {
+ Icon(
+ imageVector = Icons.Default.HomeWork,
+ contentDescription = null,
+ modifier = Modifier.size(24.dp),
+ tint = if (isSelected)
+ MaterialTheme.colorScheme.onPrimary
+ else
+ MaterialTheme.colorScheme.onTertiaryContainer
+ )
+ Column {
+ Text(
+ text = garageName,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = if (isSelected)
+ MaterialTheme.colorScheme.onPrimary
+ else
+ MaterialTheme.colorScheme.onTertiaryContainer,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = "$itemCount items",
style = MaterialTheme.typography.bodySmall,
- color = color.copy(alpha = 0.8f)
+ color = if (isSelected)
+ MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
+ else
+ MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f)
)
}
}
@@ -440,249 +575,334 @@ fun ModernStatCard(
}
@Composable
-fun ExpandableSection(
- title: String,
- subtitle: String,
- icon: ImageVector,
- isExpanded: Boolean,
- onToggle: () -> Unit,
- content: @Composable () -> Unit
+fun SimpleItemCard(
+ item: Item,
+ garages: List,
+ onClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
- .clickable(onClick = onToggle),
- elevation = CardDefaults.cardElevation(4.dp),
- shape = RoundedCornerShape(16.dp)
+ .clickable(onClick = onClick),
+ elevation = CardDefaults.cardElevation(1.dp),
+ shape = RoundedCornerShape(8.dp)
) {
- Column(modifier = Modifier.padding(20.dp)) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ item.name,
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Bold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Spacer(Modifier.height(4.dp))
+ Text(
+ getReadableLocation(item, garages),
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+
Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(
- icon,
- null,
- tint = MaterialTheme.colorScheme.primary,
- modifier = Modifier.size(28.dp)
- )
- Spacer(Modifier.width(12.dp))
- Column {
+ Column(horizontalAlignment = Alignment.End) {
+ Row {
Text(
- title,
- style = MaterialTheme.typography.titleLarge,
+ "Qty: ",
+ style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold
)
Text(
- subtitle,
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
+ "${item.quantity}",
+ style = MaterialTheme.typography.labelMedium,
+ fontWeight = FontWeight.Normal
+ )
+ }
+ val avgPrice = ((item.minPrice ?: 0.0) + (item.maxPrice ?: 0.0)) / 2
+ if (avgPrice > 0) {
+ Text(
+ "$${String.format("%.0f", avgPrice)}",
+ style = MaterialTheme.typography.labelSmall,
+ color = Color(0xFF047857),
+ fontWeight = FontWeight.Bold
)
}
}
- Icon(
- if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
- "Toggle",
- modifier = Modifier.size(28.dp)
- )
- }
- AnimatedVisibility(
- visible = isExpanded,
- enter = expandVertically() + fadeIn(),
- exit = shrinkVertically() + fadeOut()
- ) {
- Column {
- Spacer(Modifier.height(20.dp))
- content()
- }
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .background(
+ when (item.condition.lowercase()) {
+ "new" -> Color(0xFF047857)
+ "like new" -> Color(0xFF1E40AF)
+ "good" -> Color(0xFFB45309)
+ "fair" -> Color(0xFFC2410C)
+ "poor" -> Color(0xFF9F1239)
+ else -> Color(0xFF4B5563)
+ },
+ CircleShape
+ )
+ )
}
}
}
}
-
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun AdvancedAnalyticsView(
- groupedItems: Map>,
- label: String
+fun ItemDetailPDFScreen(
+ item: Item,
+ garage: Garage?,
+ onBackClick: () -> Unit
) {
- val sortedData = groupedItems.map { (key, items) ->
- val totalValue = items.sumOf { ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 * it.quantity }
- val totalQty = items.sumOf { it.quantity }
- AnalyticsData(key, items.size, totalQty, totalValue)
- }.sortedByDescending { it.totalValue }
-
- val maxValue = sortedData.maxOfOrNull { it.totalValue } ?: 1.0
- val total = sortedData.sumOf { it.itemCount }
-
- val colors = listOf(
- Color(0xFF6200EE),
- Color(0xFF03DAC5),
- Color(0xFFFF6B6B),
- Color(0xFF4ECDC4),
- Color(0xFFFFBE0B),
- Color(0xFFFFA500)
- )
-
- Column(verticalArrangement = Arrangement.spacedBy(20.dp)) {
- // Pie Chart with Legend
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceEvenly,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Canvas(
- modifier = Modifier.size(180.dp)
- ) {
- val canvasSize = size.minDimension
- val radius = canvasSize / 2
- val center = Offset(size.width / 2, size.height / 2)
-
- var startAngle = -90f
- sortedData.forEachIndexed { index, data ->
- val sweepAngle = (data.itemCount.toFloat() / total) * 360f
- drawArc(
- color = colors[index % colors.size],
- startAngle = startAngle,
- sweepAngle = sweepAngle,
- useCenter = true,
- topLeft = Offset(center.x - radius, center.y - radius),
- size = Size(radius * 2, radius * 2)
+ androidx.compose.material3.Scaffold(
+ topBar = {
+ androidx.compose.material3.TopAppBar(
+ title = {
+ Text(
+ text = item.name,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
)
- startAngle += sweepAngle
+ },
+ navigationIcon = {
+ IconButton(onClick = onBackClick) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
}
-
- drawCircle(
- color = Color.White,
- radius = radius * 0.5f,
- center = center
+ )
+ }
+ ) { padding ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ // Item Name Header
+ item {
+ Text(
+ text = item.name,
+ style = MaterialTheme.typography.headlineLarge,
+ fontWeight = FontWeight.Bold
)
}
- Column(
- modifier = Modifier.weight(1f),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- sortedData.take(6).forEachIndexed { index, data ->
- Row(verticalAlignment = Alignment.CenterVertically) {
- Box(
- modifier = Modifier
- .size(12.dp)
- .background(colors[index % colors.size], CircleShape)
- )
- Spacer(Modifier.width(8.dp))
- Text(
- "${data.name} (${data.itemCount})",
- style = MaterialTheme.typography.bodySmall,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
+ // Model Number (if exists)
+ item.modelNumber?.let { model ->
+ item {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer
)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ "Model Number",
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
+ )
+ Text(
+ model,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
}
}
}
- }
- Divider()
+ // Basic Information Section
+ item {
+ DetailSection(title = "📋 Basic Information") {
+ DetailRow(label = "Quantity", value = item.quantity.toString())
+ DetailRow(label = "Condition", value = item.condition)
+ DetailRow(label = "Functionality", value = item.functionality)
+ DetailRow(label = "Size Category", value = item.sizeCategory)
+ }
+ }
- // Data Table
- Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text(
- label,
- fontWeight = FontWeight.Bold,
- modifier = Modifier.weight(0.3f),
- fontSize = 13.sp
- )
- Text(
- "Items",
- fontWeight = FontWeight.Bold,
- textAlign = TextAlign.Center,
- modifier = Modifier.weight(0.2f),
- fontSize = 13.sp
- )
- Text(
- "Qty",
- fontWeight = FontWeight.Bold,
- textAlign = TextAlign.Center,
- modifier = Modifier.weight(0.2f),
- fontSize = 13.sp
- )
- Text(
- "Value",
- fontWeight = FontWeight.Bold,
- textAlign = TextAlign.End,
- modifier = Modifier.weight(0.3f),
- fontSize = 13.sp
- )
+ // Location Information
+ item {
+ DetailSection(title = "📍 Location") {
+ garage?.let { g ->
+ DetailRow(label = "Garage", value = g.name)
+
+ val cabinet = g.cabinets.find { it.id == item.cabinetId }
+ cabinet?.let { c ->
+ DetailRow(label = "Cabinet", value = c.name)
+
+ val shelf = c.shelves.find { it.id == item.shelfId }
+ shelf?.let { s ->
+ DetailRow(label = "Shelf", value = s.name)
+
+ val box = s.boxes.find { it.id == item.boxId }
+ box?.let { b ->
+ DetailRow(label = "Box", value = b.name)
+ }
+ }
+ }
+ } ?: run {
+ DetailRow(label = "Location", value = "Unknown")
+ }
+
+ Spacer(Modifier.height(8.dp))
+ Text(
+ "Full Path: ${getReadableLocation(item, listOfNotNull(garage))}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Medium
+ )
+ }
}
- sortedData.forEachIndexed { index, data ->
- Column {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 8.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Row(
- modifier = Modifier.weight(0.3f),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Box(
- modifier = Modifier
- .size(8.dp)
- .background(colors[index % colors.size], CircleShape)
+ // Physical Specifications
+ if (item.dimensions != null || item.weight != null) {
+ item {
+ DetailSection(title = "📏 Physical Specifications") {
+ item.dimensions?.let {
+ DetailRow(label = "Dimensions", value = it)
+ }
+ item.weight?.let {
+ DetailRow(label = "Weight", value = "$it lbs")
+ }
+ }
+ }
+ }
+
+ // Pricing Information
+ val avgPrice = ((item.minPrice ?: 0.0) + (item.maxPrice ?: 0.0)) / 2
+ if (avgPrice > 0) {
+ item {
+ DetailSection(title = "💰 Pricing") {
+ item.minPrice?.let {
+ DetailRow(
+ label = "Minimum Price",
+ value = "$${String.format("%.2f", it)}"
)
- Spacer(Modifier.width(6.dp))
- Text(
- data.name,
- fontSize = 13.sp,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
+ }
+ item.maxPrice?.let {
+ DetailRow(
+ label = "Maximum Price",
+ value = "$${String.format("%.2f", it)}"
)
}
- Text(
- "${data.itemCount}",
- textAlign = TextAlign.Center,
- modifier = Modifier.weight(0.2f),
- fontSize = 13.sp
+ DetailRow(
+ label = "Estimated Value (Avg)",
+ value = "$${String.format("%.2f", avgPrice)}",
+ highlighted = true
)
- Text(
- "${data.totalQuantity}",
- textAlign = TextAlign.Center,
- modifier = Modifier.weight(0.2f),
- fontSize = 13.sp
+ DetailRow(
+ label = "Total Value (× ${item.quantity})",
+ value = "$${String.format("%.2f", avgPrice * item.quantity)}",
+ highlighted = true
)
+ }
+ }
+ }
+
+ // Description
+ item.description?.let { desc ->
+ if (desc.isNotBlank()) {
+ item {
+ DetailSection(title = "📝 Description") {
+ Text(
+ text = desc,
+ style = MaterialTheme.typography.bodyMedium,
+ lineHeight = 24.sp
+ )
+ }
+ }
+ }
+ }
+
+ // Web Link
+ item.webLink?.let { link ->
+ if (link.isNotBlank()) {
+ item {
+ DetailSection(title = "🔗 Web Link") {
+ Text(
+ text = link,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Medium
+ )
+ }
+ }
+ }
+ }
+
+ // Images Section
+ if (item.images.isNotEmpty()) {
+ item {
+ DetailSection(title = "📷 Images") {
Text(
- "$${String.format("%.0f", data.totalValue)}",
- textAlign = TextAlign.End,
- modifier = Modifier.weight(0.3f),
- fontSize = 13.sp,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.primary
+ "${item.images.size} image(s) attached",
+ style = MaterialTheme.typography.bodyMedium
)
}
+ }
+ }
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(8.dp)
- .clip(RoundedCornerShape(4.dp))
- .background(MaterialTheme.colorScheme.surfaceVariant)
- ) {
- Box(
- modifier = Modifier
- .fillMaxWidth(fraction = (data.totalValue / maxValue).toFloat())
- .height(8.dp)
- .clip(RoundedCornerShape(4.dp))
- .background(colors[index % colors.size])
+ // Summary Statistics Card
+ item {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.secondaryContainer
+ ),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Text(
+ "📊 Quick Stats",
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSecondaryContainer
)
+ Spacer(Modifier.height(16.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ StatBadge(
+ label = "Quantity",
+ value = item.quantity.toString(),
+ icon = Icons.Default.Numbers
+ )
+ if (avgPrice > 0) {
+ StatBadge(
+ label = "Value",
+ value = "$${String.format("%.0f", avgPrice * item.quantity)}",
+ icon = Icons.Default.AttachMoney
+ )
+ }
+ item.weight?.let { w ->
+ StatBadge(
+ label = "Weight",
+ value = "${String.format("%.1f", w * item.quantity)} lbs",
+ icon = Icons.Default.Scale
+ )
+ }
+ }
}
}
}
@@ -691,85 +911,85 @@ fun AdvancedAnalyticsView(
}
@Composable
-fun ModernItemCard(
- item: Item,
- garages: List
+fun DetailSection(
+ title: String,
+ content: @Composable () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
- elevation = CardDefaults.cardElevation(2.dp),
- shape = RoundedCornerShape(12.dp)
+ elevation = CardDefaults.cardElevation(2.dp)
) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Column(modifier = Modifier.weight(1f)) {
- Text(
- item.name,
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Bold,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- Spacer(Modifier.height(4.dp))
- Text(
- getReadableLocation(item, garages),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- }
-
- Spacer(Modifier.width(16.dp))
-
- Row(
- horizontalArrangement = Arrangement.spacedBy(12.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Column(horizontalAlignment = Alignment.End) {
- Text(
- "Qty: ${item.quantity}",
- style = MaterialTheme.typography.bodyMedium,
- fontWeight = FontWeight.SemiBold
- )
- val avgPrice = ((item.minPrice ?: 0.0) + (item.maxPrice ?: 0.0)) / 2
- if (avgPrice > 0) {
- Text(
- "$${String.format("%.0f", avgPrice)}",
- style = MaterialTheme.typography.bodySmall,
- color = Color(0xFF047857),
- fontWeight = FontWeight.Bold
- )
- }
- }
-
- ConditionIndicator(item.condition)
- }
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.primary
+ )
+ Spacer(Modifier.height(12.dp))
+ content()
}
}
}
@Composable
-fun ConditionIndicator(condition: String) {
- val color = when (condition.lowercase()) {
- "new" -> Color(0xFF047857)
- "like new" -> Color(0xFF1E40AF)
- "good" -> Color(0xFFB45309)
- "fair" -> Color(0xFFC2410C)
- "poor" -> Color(0xFF9F1239)
- else -> Color(0xFF4B5563)
+fun DetailRow(
+ label: String,
+ value: String,
+ highlighted: Boolean = false
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 6.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Medium,
+ modifier = Modifier.weight(0.5f)
+ )
+ Text(
+ text = value,
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = if (highlighted) FontWeight.Bold else FontWeight.Normal,
+ color = if (highlighted) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.weight(0.5f),
+ textAlign = TextAlign.End
+ )
}
+}
- Box(
- modifier = Modifier
- .size(12.dp)
- .background(color, CircleShape)
- )
+@Composable
+fun StatBadge(
+ label: String,
+ value: String,
+ icon: ImageVector
+) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ modifier = Modifier.size(28.dp),
+ tint = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ Text(
+ text = value,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ Text(
+ text = label,
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.7f)
+ )
+ }
}
fun getReadableLocation(item: Item, garages: List): String {
@@ -785,11 +1005,4 @@ fun getReadableLocation(item: Item, garages: List): String {
} else {
"${cabinet.name} > $shelfAndBox"
}
-}
-
-data class AnalyticsData(
- val name: String,
- val itemCount: Int,
- val totalQuantity: Int,
- val totalValue: Double
-)
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/SearchScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/SearchScreen.kt
index cae82d9..a96e0b7 100644
--- a/app/src/main/java/com/samuel/inventorymanager/screens/SearchScreen.kt
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/SearchScreen.kt
@@ -10,6 +10,7 @@ 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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -24,9 +25,16 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material.icons.filled.KeyboardArrowRight
+import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Share
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -37,13 +45,18 @@ import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -51,18 +64,29 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchScreen(
items: List
- ,
garages: List,
- onItemClick: ((Item) -> Unit)? = null
+ onItemClick: ((Item) -> Unit)? = null,
+ onEditItem: ((Item) -> Unit)? = null,
+ onDeleteItem: ((Item) -> Unit)? = null,
+ onDuplicateItem: ((Item) -> Unit)? = null,
+ onShareItem: ((Item) -> Unit)? = null
) {
var searchQuery by remember { mutableStateOf("") }
var selectedCategory by remember { mutableStateOf("All Categories") }
var selectedLocation by remember { mutableStateOf("All Locations") }
+ var selectedCondition by remember { mutableStateOf("All Conditions") }
var isFiltersExpanded by remember { mutableStateOf(false) }
+ var selectedItem by remember { mutableStateOf
- (null) }
+ var showItemActionsSheet by remember { mutableStateOf(false) }
+ var showDeleteDialog by remember { mutableStateOf(false) }
+
// Create dropdown options
val categories = remember(items) {
listOf("All Categories") + items.map { it.sizeCategory }.distinct().sorted()
@@ -72,21 +96,28 @@ fun SearchScreen(
listOf("All Locations") + garages.map { it.name }.sorted()
}
+ val conditions = listOf("All Conditions", "Excellent", "Good", "Fair", "Poor")
+
// Filter items based on search criteria
- val filteredItems = remember(searchQuery, selectedCategory, selectedLocation, items) {
- items.filter { item ->
- val matchesSearch = searchQuery.isBlank() ||
- item.name.contains(searchQuery, ignoreCase = true) ||
- item.modelNumber?.contains(searchQuery, ignoreCase = true) == true ||
- item.description?.contains(searchQuery, ignoreCase = true) == true
+ val filteredItems by remember(searchQuery, selectedCategory, selectedLocation, selectedCondition, items) {
+ derivedStateOf {
+ items.filter { item ->
+ val matchesSearch = searchQuery.isBlank() ||
+ item.name.contains(searchQuery, ignoreCase = true) ||
+ item.modelNumber?.contains(searchQuery, ignoreCase = true) == true ||
+ item.description?.contains(searchQuery, ignoreCase = true) == true
- val matchesCategory = selectedCategory == "All Categories" ||
- item.sizeCategory == selectedCategory
+ val matchesCategory = selectedCategory == "All Categories" ||
+ item.sizeCategory == selectedCategory
- val matchesLocation = selectedLocation == "All Locations" ||
- garages.find { it.id == item.garageId }?.name == selectedLocation
+ val matchesLocation = selectedLocation == "All Locations" ||
+ garages.find { it.id == item.garageId }?.name == selectedLocation
- matchesSearch && matchesCategory && matchesLocation
+ val matchesCondition = selectedCondition == "All Conditions" ||
+ item.condition == selectedCondition
+
+ matchesSearch && matchesCategory && matchesLocation && matchesCondition
+ }.sortedBy { it.name }
}
}
@@ -187,6 +218,17 @@ fun SearchScreen(
modifier = Modifier.fillMaxWidth()
)
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // Condition Dropdown
+ DropdownMenuField(
+ label = "Condition",
+ selectedValue = selectedCondition,
+ options = conditions,
+ onValueChange = { selectedCondition = it },
+ modifier = Modifier.fillMaxWidth()
+ )
+
Spacer(modifier = Modifier.height(16.dp))
// Clear Filters Button
@@ -194,6 +236,7 @@ fun SearchScreen(
onClick = {
selectedCategory = "All Categories"
selectedLocation = "All Locations"
+ selectedCondition = "All Conditions"
},
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
@@ -227,7 +270,8 @@ fun SearchScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
- if (searchQuery.isNotEmpty() || selectedCategory != "All Categories" || selectedLocation != "All Locations") {
+ if (searchQuery.isNotEmpty() || selectedCategory != "All Categories" ||
+ selectedLocation != "All Locations" || selectedCondition != "All Conditions") {
Text(
text = "Clear All",
style = MaterialTheme.typography.labelMedium,
@@ -237,6 +281,7 @@ fun SearchScreen(
searchQuery = ""
selectedCategory = "All Categories"
selectedLocation = "All Locations"
+ selectedCondition = "All Conditions"
}
)
}
@@ -260,7 +305,8 @@ fun SearchScreen(
)
Spacer(modifier = Modifier.height(16.dp))
Text(
- text = if (searchQuery.isEmpty() && selectedCategory == "All Categories" && selectedLocation == "All Locations") {
+ text = if (searchQuery.isEmpty() && selectedCategory == "All Categories" &&
+ selectedLocation == "All Locations" && selectedCondition == "All Conditions") {
"Enter search terms or use filters"
} else {
"No items found"
@@ -273,21 +319,234 @@ fun SearchScreen(
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
- contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
+ contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
- items(filteredItems) { item ->
- CleanItemCard(
+ items(filteredItems, key = { it.id }) { item ->
+ ImprovedItemCard(
item = item,
garages = garages,
- onClick = { onItemClick?.invoke(item) }
+ onClick = {
+ selectedItem = item
+ showItemActionsSheet = true
+ }
+ )
+ }
+ }
+ }
+ }
+
+ // Item Actions Bottom Sheet
+ if (showItemActionsSheet && selectedItem != null) {
+ ItemActionsBottomSheet(
+ item = selectedItem!!,
+ garages = garages,
+ onDismiss = { showItemActionsSheet = false },
+ onEdit = {
+ showItemActionsSheet = false
+ onEditItem?.invoke(selectedItem!!)
+ },
+ onView = {
+ showItemActionsSheet = false
+ onItemClick?.invoke(selectedItem!!)
+ },
+ onDuplicate = {
+ showItemActionsSheet = false
+ onDuplicateItem?.invoke(selectedItem!!)
+ },
+ onShare = {
+ showItemActionsSheet = false
+ onShareItem?.invoke(selectedItem!!)
+ },
+ onDelete = {
+ showItemActionsSheet = false
+ showDeleteDialog = true
+ }
+ )
+ }
+
+ // Delete Confirmation Dialog
+ if (showDeleteDialog && selectedItem != null) {
+ AlertDialog(
+ onDismissRequest = { showDeleteDialog = false },
+ title = { Text("Delete Item?") },
+ text = { Text("Are you sure you want to delete '${selectedItem!!.name}'? This action cannot be undone.") },
+ confirmButton = {
+ Button(
+ onClick = {
+ onDeleteItem?.invoke(selectedItem!!)
+ showDeleteDialog = false
+ selectedItem = null
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Text("Delete")
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showDeleteDialog = false }) {
+ Text("Cancel")
+ }
+ }
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun ItemActionsBottomSheet(
+ item: Item,
+ garages: List,
+ onDismiss: () -> Unit,
+ onEdit: () -> Unit,
+ onView: () -> Unit,
+ onDuplicate: () -> Unit,
+ onShare: () -> Unit,
+ onDelete: () -> Unit
+) {
+ val sheetState = rememberModalBottomSheetState()
+ val scope = rememberCoroutineScope()
+
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ sheetState = sheetState,
+ shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 32.dp)
+ ) {
+ // Header with item info
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp, vertical = 16.dp)
+ ) {
+ Text(
+ text = item.name,
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ if (item.modelNumber != null) {
+ Text(
+ text = "Model: ${item.modelNumber}",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(top = 4.dp)
)
}
+
+ val locationPath = buildLocationPath(item, garages)
+ Text(
+ text = locationPath,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(top = 8.dp)
+ )
}
+
+ // Action Items
+ ActionSheetItem(
+ icon = Icons.Default.Edit,
+ title = "Edit Item",
+ description = "Modify item details and information",
+ onClick = {
+ scope.launch {
+ sheetState.hide()
+ onEdit()
+ }
+ }
+ )
+
+ ActionSheetItem(
+ icon = Icons.Default.ContentCopy,
+ title = "Duplicate Item",
+ description = "Create a copy of this item",
+ onClick = {
+ scope.launch {
+ sheetState.hide()
+ onDuplicate()
+ }
+ }
+ )
+
+ ActionSheetItem(
+ icon = Icons.Default.Share,
+ title = "Share Item",
+ description = "Share item details with others",
+ onClick = {
+ scope.launch {
+ sheetState.hide()
+ onShare()
+ }
+ }
+ )
+
+ ActionSheetItem(
+ icon = Icons.Default.Delete,
+ title = "Delete Item",
+ description = "Permanently remove this item",
+ isDestructive = true,
+ onClick = {
+ scope.launch {
+ sheetState.hide()
+ onDelete()
+ }
+ }
+ )
}
}
}
+@Composable
+private fun ActionSheetItem(
+ icon: androidx.compose.ui.graphics.vector.ImageVector,
+ title: String,
+ description: String,
+ isDestructive: Boolean = false,
+ onClick: () -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick)
+ .padding(horizontal = 24.dp, vertical = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = if (isDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(24.dp)
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold,
+ color = if (isDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface
+ )
+ Text(
+ text = description,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Icon(
+ imageVector = Icons.Default.KeyboardArrowRight,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
+ )
+ }
+}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DropdownMenuField(
@@ -301,7 +560,7 @@ private fun DropdownMenuField(
ExposedDropdownMenuBox(
expanded = expanded,
- onExpandedChange = { expanded = !expanded },
+ onExpandedChange = { expanded = it },
modifier = modifier
) {
OutlinedTextField(
@@ -334,7 +593,7 @@ private fun DropdownMenuField(
}
@Composable
-private fun CleanItemCard(
+private fun ImprovedItemCard(
item: Item,
garages: List,
onClick: () -> Unit
@@ -432,8 +691,8 @@ private fun CleanItemCard(
// Right Arrow
Icon(
- imageVector = Icons.Default.KeyboardArrowRight,
- contentDescription = "View Details",
+ imageVector = Icons.Default.MoreVert,
+ contentDescription = "More Options",
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
modifier = Modifier.size(24.dp)
)
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/SettingScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/SettingScreen.kt
index cc87f1f..a291938 100644
--- a/app/src/main/java/com/samuel/inventorymanager/screens/SettingScreen.kt
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/SettingScreen.kt
@@ -1,9 +1,10 @@
+// *** REPLACE THE ENTIRE CONTENTS of SettingsScreen.kt with this final code ***
+
@file:Suppress("DEPRECATION")
package com.samuel.inventorymanager.screens
import android.Manifest
-import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
@@ -33,30 +34,18 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowDownward
-import androidx.compose.material.icons.filled.ArrowUpward
-import androidx.compose.material.icons.filled.Backup
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.CheckCircle
-import androidx.compose.material.icons.filled.Cloud
-import androidx.compose.material.icons.filled.DocumentScanner
+import androidx.compose.material.icons.filled.DeleteForever
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.Folder
-import androidx.compose.material.icons.filled.Login
-import androidx.compose.material.icons.filled.Logout
import androidx.compose.material.icons.filled.Palette
-import androidx.compose.material.icons.filled.Psychology
-import androidx.compose.material.icons.filled.RestartAlt
-import androidx.compose.material.icons.filled.Save
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material.icons.filled.Upload
-import androidx.compose.material.icons.filled.Visibility
-import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
@@ -67,15 +56,13 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
-import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Slider
-import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
@@ -91,46 +78,73 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.PasswordVisualTransformation
-import androidx.compose.ui.text.input.VisualTransformation
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
-import com.google.android.gms.auth.api.signin.GoogleSignIn
-import com.google.android.gms.common.api.ApiException
-import com.samuel.inventorymanager.auth.GoogleAuthManager
-import com.samuel.inventorymanager.data.AISettings
+import androidx.core.net.toUri
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import com.google.gson.Gson
import com.samuel.inventorymanager.data.AppSettings
import com.samuel.inventorymanager.data.AppTheme
-import com.samuel.inventorymanager.data.AutoFeatures
import com.samuel.inventorymanager.data.CustomTheme
import com.samuel.inventorymanager.data.FontSize
-import com.samuel.inventorymanager.data.GoogleSettings
-import com.samuel.inventorymanager.data.OCRSettings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import java.io.OutputStreamWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
+// Helper function to check permission state
+private fun checkStoragePermission(context: Context): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ Environment.isExternalStorageManager()
+ } else {
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+}
+
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
currentSettings: AppSettings,
- onSettingsChange: (AppSettings) -> Unit
+ currentData: AppData,
+ onSettingsChange: (AppSettings) -> Unit,
+ onDataChange: (AppData) -> Unit,
+ onClearAllData: () -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
- val authManager = remember { GoogleAuthManager(context) }
+ val lifecycleOwner = LocalLifecycleOwner.current
var settings by remember { mutableStateOf(currentSettings) }
var showColorPicker by remember { mutableStateOf(false) }
- var showResetDialog by remember { mutableStateOf(false) }
+ var showClearDataDialog by remember { mutableStateOf(false) }
var isProcessing by remember { mutableStateOf(false) }
var feedbackMessage by remember { mutableStateOf(null) }
+ var hasStoragePermission by remember { mutableStateOf(checkStoragePermission(context)) }
+
+ // This observer re-checks the permission every time the user returns to this screen. THIS IS THE FIX.
+ DisposableEffect(lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ if (event == Lifecycle.Event.ON_RESUME) {
+ hasStoragePermission = checkStoragePermission(context)
+ }
+ }
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+
LaunchedEffect(currentSettings) {
settings = currentSettings
@@ -139,154 +153,105 @@ fun SettingsScreen(
LaunchedEffect(feedbackMessage) {
feedbackMessage?.let { message ->
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
- kotlinx.coroutines.delay(2000)
+ kotlinx.coroutines.delay(2500)
feedbackMessage = null
}
}
val storagePermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
- ) { permissions ->
- val granted = permissions.values.all { it }
- feedbackMessage = if (granted) "✅ Storage permission granted" else "❌ Storage permission denied"
+ ) {
+ hasStoragePermission = checkStoragePermission(context)
+ feedbackMessage = if (hasStoragePermission) "✅ Storage granted" else "❌ Storage denied"
}
val manageStorageLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
- val granted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- Environment.isExternalStorageManager()
- } else true
- feedbackMessage = if (granted) "✅ Storage access granted" else "❌ Storage access denied"
+ // This runs after the user returns from the system settings screen
+ hasStoragePermission = checkStoragePermission(context)
+ feedbackMessage = if (hasStoragePermission) "✅ Storage permission granted!" else "❌ Storage permission was not granted."
}
val requestStoragePermission = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
- data = Uri.parse("package:${context.packageName}")
+ data = "package:${context.packageName}".toUri()
}
manageStorageLauncher.launch(intent)
+ } else {
+ feedbackMessage = "✅ Storage permission is already granted."
}
} else {
storagePermissionLauncher.launch(
- arrayOf(
- Manifest.permission.READ_EXTERNAL_STORAGE,
- Manifest.permission.WRITE_EXTERNAL_STORAGE
- )
+ arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)
)
}
}
- // Firebase Google Sign-In Launcher
- val googleSignInLauncher = rememberLauncherForActivityResult(
- ActivityResultContracts.StartActivityForResult()
- ) { result ->
- if (result.resultCode == Activity.RESULT_OK) {
- val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
- try {
- val account = task.getResult(ApiException::class.java)
- val idToken = account?.idToken
-
- if (idToken != null) {
- authManager.handleSignInResult(
- idToken,
- onSuccess = { message ->
- settings = settings.copy(
- googleSettings = settings.googleSettings.copy(
- signedIn = true,
- userEmail = authManager.getCurrentUserEmail()
- )
- )
- onSettingsChange(settings)
- feedbackMessage = message
- },
- onError = { error ->
- feedbackMessage = error
- }
- )
- }
- } catch (e: ApiException) {
- feedbackMessage = "❌ Sign in failed: ${e.message}"
+ val jsonImportLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
+ uri?.let {
+ scope.launch {
+ isProcessing = true
+ val result = importAppDataFromJson(context, it)
+ isProcessing = false
+ result?.let { importedData ->
+ onDataChange(importedData)
+ feedbackMessage = "✅ Data imported successfully"
+ } ?: run { feedbackMessage = "❌ Import failed. Invalid file." }
}
- } else {
- feedbackMessage = "❌ Sign in cancelled"
}
}
- val onSignInClick: () -> Unit = {
- val signInIntent = authManager.getSignInIntent()
- googleSignInLauncher.launch(signInIntent)
- }
-
- val importLauncher = rememberLauncherForActivityResult(
- ActivityResultContracts.GetContent()
+ val jsonExportLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.CreateDocument("application/json")
) { uri: Uri? ->
uri?.let {
scope.launch {
isProcessing = true
- val result = importSettings(context, it)
+ val success = exportAppDataToJson(context, currentData, it)
isProcessing = false
- result?.let { importedSettings ->
- settings = importedSettings
- onSettingsChange(importedSettings)
- feedbackMessage = "✅ Settings imported successfully"
- } ?: run {
- feedbackMessage = "❌ Failed to import settings"
- }
+ feedbackMessage = if (success) "✅ Backup exported successfully" else "❌ Export failed"
}
}
}
- val exportLauncher = rememberLauncherForActivityResult(
- ActivityResultContracts.CreateDocument("application/json")
+ val csvExportLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.CreateDocument("text/csv")
) { uri: Uri? ->
uri?.let {
scope.launch {
isProcessing = true
- val success = exportSettings(context, settings, it)
+ val success = exportAppDataToCsv(context, currentData, it)
isProcessing = false
- feedbackMessage = if (success) "✅ Settings exported successfully"
- else "❌ Failed to export settings"
+ feedbackMessage = if (success) "✅ CSV exported successfully" else "❌ Export failed"
}
}
}
- var themeExpanded by remember { mutableStateOf(true) }
- var ocrExpanded by remember { mutableStateOf(false) }
- var aiExpanded by remember { mutableStateOf(false) }
- var googleExpanded by remember { mutableStateOf(false) }
- var dataExpanded by remember { mutableStateOf(false) }
+ var themeExpanded by remember { mutableStateOf(false) }
+ var dataExpanded by remember { mutableStateOf(true) }
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .padding(16.dp),
+ modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
- feedbackMessage?.let { message ->
- Card(
- modifier = Modifier.fillMaxWidth(),
- colors = CardDefaults.cardColors(
- containerColor = if (message.startsWith("✅"))
- MaterialTheme.colorScheme.primaryContainer
- else MaterialTheme.colorScheme.errorContainer
- )
- ) {
- Text(
- message,
- modifier = Modifier.padding(16.dp),
- style = MaterialTheme.typography.bodyMedium
- )
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
+ ) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Text("⚙️ Settings", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onPrimaryContainer)
+ Text("Manage themes, data, and backups.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onPrimaryContainer)
}
}
}
-
item {
ExpandableCard(
- title = "Theme Settings",
+ title = "🎨 Theme & Appearance",
icon = Icons.Default.Palette,
expanded = themeExpanded,
onToggle = { themeExpanded = !themeExpanded }
@@ -300,149 +265,49 @@ fun SettingsScreen(
onFontSizeChange = { newFontSize ->
settings = settings.copy(fontSize = newFontSize)
onSettingsChange(settings)
- },
- onCustomThemeClick = { showColorPicker = true }
- )
- }
- }
-
- item {
- ExpandableCard(
- title = "OCR Settings & Fallback Priority",
- icon = Icons.Default.DocumentScanner,
- expanded = ocrExpanded,
- onToggle = { ocrExpanded = !ocrExpanded }
- ) {
- OCRSettingsContent(
- ocrSettings = settings.ocrSettings,
- onUpdate = { newOcrSettings ->
- settings = settings.copy(ocrSettings = newOcrSettings)
- onSettingsChange(settings)
- }
- )
- }
- }
-
- item {
- ExpandableCard(
- title = "AI Settings & Fallback Priority",
- icon = Icons.Default.Psychology,
- expanded = aiExpanded,
- onToggle = { aiExpanded = !aiExpanded }
- ) {
- AISettingsContent(
- aiSettings = settings.aiSettings,
- onUpdate = { newAiSettings ->
- settings = settings.copy(aiSettings = newAiSettings)
- onSettingsChange(settings)
}
)
}
}
-
item {
ExpandableCard(
- title = "Google Settings",
- icon = Icons.Default.Cloud,
- expanded = googleExpanded,
- onToggle = { googleExpanded = !googleExpanded }
- ) {
- GoogleSettingsContent(
- authManager = authManager,
- googleSettings = settings.googleSettings,
- onSignInClick = onSignInClick,
- onUpdate = { newGoogleSettings ->
- settings = settings.copy(googleSettings = newGoogleSettings)
- onSettingsChange(settings)
- },
- onBackupNow = {
- scope.launch {
- isProcessing = true
- authManager.uploadToDrive(
- "inventory_backup_${System.currentTimeMillis()}.json",
- onSuccess = { message ->
- isProcessing = false
- val updated = settings.copy(
- googleSettings = settings.googleSettings.copy(
- lastBackupTime = System.currentTimeMillis()
- )
- )
- settings = updated
- onSettingsChange(updated)
- feedbackMessage = message
- }
- )
- }
- }
- )
- }
- }
-
- item {
- ExpandableCard(
- title = "Data Management & Android Features",
+ title = "💾 Data Management",
icon = Icons.Default.Storage,
expanded = dataExpanded,
onToggle = { dataExpanded = !dataExpanded }
) {
DataManagementContent(
- autoFeatures = settings.autoFeatures,
+ hasPermission = hasStoragePermission, // Pass the reactive state here
onRequestPermissions = requestStoragePermission,
- onUpdate = { newAutoFeatures ->
- settings = settings.copy(autoFeatures = newAutoFeatures)
- onSettingsChange(settings)
+ onExportJson = {
+ val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
+ jsonExportLauncher.launch("inventory_backup_$timestamp.json")
},
- onExport = {
- val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
- .format(Date())
- exportLauncher.launch("inventory_settings_$timestamp.json")
- },
- onImport = {
- importLauncher.launch("application/json")
- },
- onLocalSaveNow = {
- scope.launch {
- isProcessing = true
- val success = performLocalSave(context, settings)
- isProcessing = false
- if (success) {
- val updated = settings.copy(
- autoFeatures = settings.autoFeatures.copy(
- lastLocalSaveTime = System.currentTimeMillis()
- )
- )
- settings = updated
- onSettingsChange(updated)
- feedbackMessage = "✅ Local save completed"
- } else {
- feedbackMessage = "❌ Local save failed"
- }
- }
+ onImportJson = { jsonImportLauncher.launch("application/json") },
+ onExportCsv = {
+ val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
+ csvExportLauncher.launch("inventory_export_$timestamp.csv")
}
)
}
}
-
+ item { Spacer(Modifier.height(16.dp)) }
item {
- Button(
- onClick = { showResetDialog = true },
+ OutlinedButton(
+ onClick = { showClearDataDialog = true },
modifier = Modifier.fillMaxWidth(),
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.error
- )
+ colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.error),
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.error)
) {
- Icon(Icons.Default.RestartAlt, null)
+ Icon(Icons.Default.DeleteForever, null)
Spacer(Modifier.width(8.dp))
- Text("Reset All Settings")
+ Text("Clear All Data")
}
}
}
-
if (isProcessing) {
Box(
- Modifier
- .fillMaxSize()
- .background(Color.Black.copy(alpha = 0.5f)),
+ Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.5f)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
@@ -462,41 +327,33 @@ fun SettingsScreen(
)
}
- if (showResetDialog) {
+ if (showClearDataDialog) {
AlertDialog(
- onDismissRequest = { showResetDialog = false },
- title = { Text("Reset Settings") },
- text = { Text("Are you sure you want to reset all settings to default?") },
+ onDismissRequest = { showClearDataDialog = false },
+ title = { Text("Clear All Data?") },
+ text = { Text("This will permanently delete all your inventory data and settings. This action cannot be undone.") },
confirmButton = {
Button(
onClick = {
- settings = AppSettings()
- onSettingsChange(settings)
- showResetDialog = false
- feedbackMessage = "✅ Settings reset to default"
+ onClearAllData()
+ showClearDataDialog = false
+ feedbackMessage = "✅ All application data has been cleared."
},
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.error
- )
- ) { Text("Reset") }
+ colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
+ ) { Text("Clear Everything") }
},
- dismissButton = {
- TextButton({ showResetDialog = false }) { Text("Cancel") }
- }
+ dismissButton = { TextButton({ showClearDataDialog = false }) { Text("Cancel") } }
)
}
}
-// ========================================================================================
-// HELPER FUNCTIONS
-// ========================================================================================
+// DATA I/O FUNCTIONS and UI HELPERS remain the same, just moved outside the main composable
-suspend fun exportSettings(context: Context, settings: AppSettings, uri: Uri): Boolean {
+private suspend fun exportAppDataToJson(context: Context, appData: AppData, uri: Uri): Boolean {
return withContext(Dispatchers.IO) {
try {
- context.contentResolver.openOutputStream(uri)?.use { outputStream ->
- outputStream.write(settings.toJson().toByteArray())
- }
+ val jsonString = Gson().toJson(appData)
+ context.contentResolver.openOutputStream(uri)?.use { it.write(jsonString.toByteArray()) }
true
} catch (e: Exception) {
e.printStackTrace()
@@ -505,12 +362,12 @@ suspend fun exportSettings(context: Context, settings: AppSettings, uri: Uri): B
}
}
-suspend fun importSettings(context: Context, uri: Uri): AppSettings? {
+private suspend fun importAppDataFromJson(context: Context, uri: Uri): AppData? {
return withContext(Dispatchers.IO) {
try {
- context.contentResolver.openInputStream(uri)?.use { inputStream ->
- val json = inputStream.bufferedReader().readText()
- AppSettings.fromJson(json)
+ context.contentResolver.openInputStream(uri)?.use {
+ val json = it.bufferedReader().readText()
+ Gson().fromJson(json, AppData::class.java)
}
} catch (e: Exception) {
e.printStackTrace()
@@ -519,11 +376,31 @@ suspend fun importSettings(context: Context, uri: Uri): AppSettings? {
}
}
-suspend fun performLocalSave(context: Context, settings: AppSettings): Boolean {
+private suspend fun exportAppDataToCsv(context: Context, appData: AppData, uri: Uri): Boolean {
return withContext(Dispatchers.IO) {
try {
- val file = java.io.File(context.filesDir, "app_settings.json")
- file.writeText(settings.toJson())
+ val garageMap = appData.garages.associateBy { it.id }
+ val cabinetMap = appData.garages.flatMap { it.cabinets }.associateBy { it.id }
+ val shelfMap = appData.garages.flatMap { it.cabinets }.flatMap { it.shelves }.associateBy { it.id }
+ val boxMap = appData.garages.flatMap { it.cabinets }.flatMap { it.shelves }.flatMap { it.boxes }.associateBy { it.id }
+
+ context.contentResolver.openOutputStream(uri)?.use { stream ->
+ OutputStreamWriter(stream).use { writer ->
+ writer.appendLine("\"itemID\",\"itemName\",\"quantity\",\"condition\",\"functionality\",\"garageName\",\"cabinetName\",\"shelfName\",\"boxName\",\"modelNumber\",\"description\",\"webLink\",\"minPrice\",\"maxPrice\",\"weight\",\"sizeCategory\",\"dimensions\"")
+ appData.items.forEach { item ->
+ val row = listOf(
+ item.id, item.name, item.quantity.toString(), item.condition,
+ item.functionality, garageMap[item.garageId]?.name ?: "N/A",
+ cabinetMap[item.cabinetId]?.name ?: "N/A", shelfMap[item.shelfId]?.name ?: "N/A",
+ item.boxId?.let { boxMap[it]?.name } ?: "", item.modelNumber ?: "",
+ item.description ?: "", item.webLink ?: "",
+ item.minPrice?.toString() ?: "", item.maxPrice?.toString() ?: "",
+ item.weight?.toString() ?: "", item.sizeCategory, item.dimensions ?: ""
+ ).joinToString(",") { "\"${it.replace("\"", "\"\"")}\"" }
+ writer.appendLine(row)
+ }
+ }
+ }
true
} catch (e: Exception) {
e.printStackTrace()
@@ -532,48 +409,25 @@ suspend fun performLocalSave(context: Context, settings: AppSettings): Boolean {
}
}
-// ========================================================================================
-// COMPOSABLE COMPONENTS
-// ========================================================================================
-
@Composable
-fun ExpandableCard(
- title: String,
- icon: ImageVector,
- expanded: Boolean,
- onToggle: () -> Unit,
+private fun ExpandableCard(
+ title: String, icon: ImageVector, expanded: Boolean, onToggle: () -> Unit,
content: @Composable ColumnScope.() -> Unit
) {
- Card(
- modifier = Modifier.fillMaxWidth(),
- elevation = CardDefaults.cardElevation(2.dp)
- ) {
+ Card(modifier = Modifier.fillMaxWidth(), elevation = CardDefaults.cardElevation(2.dp)) {
Column {
Row(
- modifier = Modifier
- .fillMaxWidth()
- .clickable(onClick = onToggle)
- .padding(16.dp),
+ modifier = Modifier.fillMaxWidth().clickable(onClick = onToggle).padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
- Icon(icon, null, tint = MaterialTheme.colorScheme.primary)
+ Icon(icon, null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(28.dp))
Spacer(Modifier.width(16.dp))
- Text(
- title,
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Bold,
- modifier = Modifier.weight(1f)
- )
- Icon(
- if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
- null
- )
+ Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
+ Icon(if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, null)
}
AnimatedVisibility(visible = expanded) {
Column(
- Modifier
- .padding(horizontal = 16.dp, vertical = 8.dp)
- .fillMaxWidth(),
+ Modifier.padding(horizontal = 16.dp, vertical = 8.dp).fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
content = content
)
@@ -583,449 +437,26 @@ fun ExpandableCard(
}
@Composable
-fun ThemeSettingsContent(
- settings: AppSettings,
- onThemeChange: (AppTheme) -> Unit,
- onFontSizeChange: (FontSize) -> Unit,
- onCustomThemeClick: () -> Unit
+private fun DataManagementContent(
+ hasPermission: Boolean, // Takes the state as a parameter now
+ onRequestPermissions: () -> Unit, onExportJson: () -> Unit, onImportJson: () -> Unit, onExportCsv: () -> Unit
) {
- val themeOptions = listOf(
- "☀️ Light" to Color(0xFFFFFBFE),
- "🌙 Dark" to Color(0xFF1A1A1A),
- "🧛 Dracula" to Color(0xFFBD93F9),
- "🧟 Vampire" to Color(0xFFFF1493),
- "🌊 Ocean" to Color(0xFF00B4D8),
- "🌲 Forest" to Color(0xFF2D6A4F),
- "🌅 Sunset" to Color(0xFFFF6B35),
- "⚙️ Cyberpunk" to Color(0xFFFF006E),
- "⚡ Neon" to Color(0xFF39FF14)
- )
-
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
- Text("Select Theme", style = MaterialTheme.typography.labelLarge)
-
- // --- FIX START ---
- // Replace LazyVerticalGrid with a standard Column and Rows to build the grid
- Column(
- modifier = Modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- // Group the theme options into rows of 3
- themeOptions.chunked(3).forEach { rowItems ->
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- // Create a ThemeOptionCard for each item in the row, giving it equal weight
- rowItems.forEach { (name, color) ->
- Box(modifier = Modifier.weight(1f)) {
- ThemeOptionCard(name, color, settings.theme == AppTheme.CUSTOM) {
- onThemeChange(AppTheme.CUSTOM)
- onCustomThemeClick()
- }
- }
- }
- // Add invisible spacers to fill the row if it has fewer than 3 items.
- // This ensures items in the last row align correctly with the rows above.
- if (rowItems.size < 3) {
- repeat(3 - rowItems.size) {
- Spacer(modifier = Modifier.weight(1f))
- }
- }
- }
- }
- }
- // --- FIX END ---
-
- HorizontalDivider(Modifier.padding(vertical = 8.dp))
-
- Text("Font & Icon Size", style = MaterialTheme.typography.labelLarge)
- Text(
- "Select default text size",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- FontSize.entries.forEach { size ->
- FontSizeButton(
- size.name.lowercase().replaceFirstChar { it.uppercase() },
- settings.fontSize == size
- ) { onFontSizeChange(size) }
- }
- }
- }
-}
-
-@Composable
-fun ThemeOptionCard(
- themeName: String,
- themeColor: Color,
- isSelected: Boolean,
- onClick: () -> Unit
-) {
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .height(80.dp)
- .clickable(onClick = onClick),
- colors = CardDefaults.cardColors(
- containerColor = themeColor.copy(alpha = 0.2f)
- ),
- border = if (isSelected) BorderStroke(3.dp, themeColor) else null,
- elevation = CardDefaults.cardElevation(if (isSelected) 4.dp else 1.dp)
- ) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(
- brush = Brush.linearGradient(
- colors = listOf(
- themeColor.copy(alpha = 0.4f),
- themeColor.copy(alpha = 0.1f)
- )
- )
- ),
- contentAlignment = Alignment.Center
- ) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- Text(
- themeName,
- style = MaterialTheme.typography.labelMedium,
- color = Color.Black,
- fontWeight = FontWeight.Bold
- )
- if (isSelected) {
- Icon(
- Icons.Default.Check,
- null,
- tint = themeColor,
- modifier = Modifier.size(16.dp)
- )
- }
- }
- }
- }
-}
-
-@Composable
-fun RowScope.FontSizeButton(text: String, selected: Boolean, onClick: () -> Unit) {
- Button(
- onClick,
- Modifier.weight(1f),
- colors = ButtonDefaults.buttonColors(
- containerColor = if (selected)
- MaterialTheme.colorScheme.primary
- else
- MaterialTheme.colorScheme.surfaceVariant
- )
- ) {
- Text(text, fontSize = 11.sp, maxLines = 1)
- }
-}
-
-@Composable
-fun OCRSettingsContent(ocrSettings: OCRSettings, onUpdate: (OCRSettings) -> Unit) {
- Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
- Text(
- "OCR Priority (Use Fallback)",
- style = MaterialTheme.typography.titleSmall,
- fontWeight = FontWeight.Bold
- )
- Text(
- "Higher priority providers are tried first",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
-
- ocrSettings.providerPriority.forEachIndexed { index, provider ->
- ProviderPriorityItem(
- name = provider.name.replace("_", " ").lowercase()
- .split(" ").joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } },
- priority = index + 1,
- onMoveUp = if (index > 0) {
- {
- val newList = ocrSettings.providerPriority.toMutableList()
- val temp = newList[index]
- newList[index] = newList[index - 1]
- newList[index - 1] = temp
- onUpdate(ocrSettings.copy(providerPriority = newList))
- }
- } else null,
- onMoveDown = if (index < ocrSettings.providerPriority.size - 1) {
- {
- val newList = ocrSettings.providerPriority.toMutableList()
- val temp = newList[index]
- newList[index] = newList[index + 1]
- newList[index + 1] = temp
- onUpdate(ocrSettings.copy(providerPriority = newList))
- }
- } else null
- )
- }
-
- Button(
- onClick = { onUpdate(ocrSettings.copy(providerPriority = OCRSettings().providerPriority)) },
- modifier = Modifier.fillMaxWidth(),
- colors = ButtonDefaults.outlinedButtonColors()
- ) {
- Icon(Icons.Default.RestartAlt, null)
- Spacer(Modifier.width(8.dp))
- Text("Reset to Default Priority")
- }
-
- HorizontalDivider(Modifier.padding(vertical = 8.dp))
-
- APIKeyField("Roboflow API Key", ocrSettings.roboflowApiKey) {
- onUpdate(ocrSettings.copy(roboflowApiKey = it))
- }
- APIKeyField("OCR Space API Key", ocrSettings.ocrSpaceApiKey) {
- onUpdate(ocrSettings.copy(ocrSpaceApiKey = it))
- }
- APIKeyField("Google Vision API Key", ocrSettings.googleVisionApiKey) {
- onUpdate(ocrSettings.copy(googleVisionApiKey = it))
- }
- }
-}
-
-@Composable
-fun AISettingsContent(aiSettings: AISettings, onUpdate: (AISettings) -> Unit) {
- Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
- Text(
- "AI Priority (Use Fallback)",
- style = MaterialTheme.typography.titleSmall,
- fontWeight = FontWeight.Bold
- )
- Text(
- "Higher priority providers are tried first",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
-
- aiSettings.providerPriority.forEachIndexed { index, provider ->
- ProviderPriorityItem(
- name = provider.name.replace("_", " ").lowercase()
- .split(" ").joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } },
- priority = index + 1,
- onMoveUp = if (index > 0) {
- {
- val newList = aiSettings.providerPriority.toMutableList()
- val temp = newList[index]
- newList[index] = newList[index - 1]
- newList[index - 1] = temp
- onUpdate(aiSettings.copy(providerPriority = newList))
- }
- } else null,
- onMoveDown = if (index < aiSettings.providerPriority.size - 1) {
- {
- val newList = aiSettings.providerPriority.toMutableList()
- val temp = newList[index]
- newList[index] = newList[index + 1]
- newList[index + 1] = temp
- onUpdate(aiSettings.copy(providerPriority = newList))
- }
- } else null
- )
- }
-
- Icon(Icons.Default.RestartAlt, null)
- Spacer(Modifier.width(8.dp))
- Text("Reset to Default Priority")
- }
-
- HorizontalDivider(Modifier.padding(vertical = 8.dp))
-
- APIKeyField("Google Gemini API Key", aiSettings.googleGeminiApiKey) {
- onUpdate(aiSettings.copy(googleGeminiApiKey = it))
- }
- APIKeyField("ChatGPT API Key", aiSettings.openAIApiKey) {
- onUpdate(aiSettings.copy(openAIApiKey = it))
- }
-}
-
-
-@Composable
-fun GoogleSettingsContent(
- authManager: GoogleAuthManager,
- googleSettings: GoogleSettings,
- onSignInClick: () -> Unit,
- onUpdate: (GoogleSettings) -> Unit,
- onBackupNow: () -> Unit
-) {
- Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
- if (authManager.isSignedIn()) {
+ if (!hasPermission) {
Card(
modifier = Modifier.fillMaxWidth(),
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.primaryContainer
- )
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)
) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- Icons.Default.CheckCircle,
- "Signed In",
- tint = MaterialTheme.colorScheme.primary,
- modifier = Modifier.size(32.dp)
- )
- Spacer(Modifier.width(12.dp))
- Column(modifier = Modifier.weight(1f)) {
- Text(
- "✅ Signed In",
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onPrimaryContainer
- )
- Text(
- authManager.getCurrentUserEmail(),
- style = MaterialTheme.typography.bodySmall,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- color = MaterialTheme.colorScheme.onPrimaryContainer
- )
- }
- }
- }
-
- SwitchRow(
- "🔄 Auto Backup to Drive",
- googleSettings.autoBackupToDrive
- ) {
- onUpdate(googleSettings.copy(autoBackupToDrive = it))
- }
-
- if (googleSettings.lastBackupTime > 0) {
- Text(
- "Last backup: ${SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault()).format(Date(googleSettings.lastBackupTime))}",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
-
- Button(
- onClick = onBackupNow,
- modifier = Modifier.fillMaxWidth()
- ) {
- Icon(Icons.Default.Backup, null)
- Spacer(Modifier.width(8.dp))
- Text("Backup Now")
- }
-
- Button(
- onClick = {
- authManager.signOut()
- onUpdate(GoogleSettings())
- },
- modifier = Modifier.fillMaxWidth(),
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.error
- )
- ) {
- Icon(Icons.Default.Logout, null)
- Spacer(Modifier.width(8.dp))
- Text("Sign Out")
- }
- } else {
- Card(
- modifier = Modifier.fillMaxWidth(),
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.surfaceVariant
- )
- ) {
- Column(
- modifier = Modifier.padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- Text(
- "ℹ️ Why Sign In?",
- fontWeight = FontWeight.Bold
- )
- Text(
- "• Backup your inventory to Google Drive\n" +
- "• Access data across devices\n" +
- "• Never lose your data",
- style = MaterialTheme.typography.bodySmall
- )
- }
- }
-
- Button(
- onClick = onSignInClick,
- modifier = Modifier.fillMaxWidth(),
- colors = ButtonDefaults.buttonColors(
- containerColor = Color(0xFF4285F4)
- )
- ) {
- Icon(Icons.Default.Login, null)
- Spacer(Modifier.width(8.dp))
- Text("Sign in with Google")
- }
- }
- }
-}
-
-@Composable
-fun DataManagementContent(
- autoFeatures: AutoFeatures,
- onRequestPermissions: () -> Unit,
- onUpdate: (AutoFeatures) -> Unit,
- onExport: () -> Unit,
- onImport: () -> Unit,
- onLocalSaveNow: () -> Unit
-) {
- val context = LocalContext.current
- val hasStoragePermission = remember {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- Environment.isExternalStorageManager()
- } else {
- ContextCompat.checkSelfPermission(
- context,
- Manifest.permission.WRITE_EXTERNAL_STORAGE
- ) == PackageManager.PERMISSION_GRANTED
- }
- }
-
- Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
- if (!hasStoragePermission) {
- Card(
- modifier = Modifier.fillMaxWidth(),
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.errorContainer
- )
- ) {
- Column(
- modifier = Modifier.padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
+ Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(
- Icons.Default.Warning,
- "Warning",
- tint = MaterialTheme.colorScheme.error
- )
+ Icon(Icons.Default.Warning, "Warning", tint = MaterialTheme.colorScheme.error)
Spacer(Modifier.width(8.dp))
- Text(
- "Storage Permission Required",
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onErrorContainer
- )
+ Text("Storage Permission Required", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onErrorContainer)
}
- Text(
- "To save data locally, grant storage access",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onErrorContainer
- )
+ Text("To import or export data files, the app needs storage access.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onErrorContainer)
Button(
- onClick = onRequestPermissions,
- modifier = Modifier.fillMaxWidth(),
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.error
- )
+ onClick = onRequestPermissions, modifier = Modifier.fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
) {
Icon(Icons.Default.Folder, null)
Spacer(Modifier.width(8.dp))
@@ -1036,176 +467,77 @@ fun DataManagementContent(
} else {
Card(
modifier = Modifier.fillMaxWidth(),
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.primaryContainer
- )
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- Icons.Default.CheckCircle,
- "Granted",
- tint = MaterialTheme.colorScheme.primary
- )
+ Row(modifier = Modifier.fillMaxWidth().padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Default.CheckCircle, "Granted", tint = MaterialTheme.colorScheme.primary)
Spacer(Modifier.width(12.dp))
- Text(
- "✅ Storage Permission Granted",
- fontWeight = FontWeight.SemiBold,
- color = MaterialTheme.colorScheme.onPrimaryContainer
- )
+ Text("✅ Storage Permission Granted", fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onPrimaryContainer)
}
}
}
- HorizontalDivider()
-
- SwitchRow("📦 Auto Local Save", autoFeatures.autoLocalSave) {
- onUpdate(autoFeatures.copy(autoLocalSave = it))
- }
- SwitchRow("☁️ Auto Google Backup", autoFeatures.autoGoogleBackup) {
- onUpdate(autoFeatures.copy(autoGoogleBackup = it))
- }
-
- if (autoFeatures.lastLocalSaveTime > 0) {
- Text(
- "Last local save: ${SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault()).format(Date(autoFeatures.lastLocalSaveTime))}",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
-
- Button(
- onClick = onLocalSaveNow,
- modifier = Modifier.fillMaxWidth()
- ) {
- Icon(Icons.Default.Save, null)
- Spacer(Modifier.width(8.dp))
- Text("Save Locally Now")
- }
-
- HorizontalDivider()
+ HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
+ Text("Backup & Restore (JSON)", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
+ Text("Use a JSON file to create a complete backup of your app data or restore from a previous backup.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- OutlinedButton(
- onClick = onExport,
- modifier = Modifier.weight(1f)
- ) {
- Icon(Icons.Default.Upload, null)
- Spacer(Modifier.width(4.dp))
- Text("Export")
- }
- OutlinedButton(
- onClick = onImport,
- modifier = Modifier.weight(1f)
- ) {
- Icon(Icons.Default.Download, null)
- Spacer(Modifier.width(4.dp))
+ OutlinedButton(onClick = onImportJson, modifier = Modifier.weight(1f), enabled = hasPermission) {
+ Icon(Icons.Default.Download, null, modifier = Modifier.size(ButtonDefaults.IconSize))
+ Spacer(Modifier.width(8.dp))
Text("Import")
}
+ OutlinedButton(onClick = onExportJson, modifier = Modifier.weight(1f), enabled = hasPermission) {
+ Icon(Icons.Default.Upload, null, modifier = Modifier.size(ButtonDefaults.IconSize))
+ Spacer(Modifier.width(8.dp))
+ Text("Export")
+ }
}
- }
-}
-@Composable
-fun APIKeyField(label: String, value: String, onValueChange: (String) -> Unit) {
- var showKey by remember { mutableStateOf(false) }
- OutlinedTextField(
- value = value,
- onValueChange = onValueChange,
- label = { Text(label) },
- modifier = Modifier.fillMaxWidth(),
- visualTransformation = if (showKey)
- VisualTransformation.None
- else
- PasswordVisualTransformation(),
- trailingIcon = {
- IconButton({ showKey = !showKey }) {
- Icon(
- if (showKey) Icons.Default.Visibility else Icons.Default.VisibilityOff,
- if (showKey) "Hide" else "Show"
- )
- }
- },
- singleLine = true
- )
-}
+ HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
-@Composable
-fun SwitchRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
- Row(
- Modifier
- .fillMaxWidth()
- .clickable { onCheckedChange(!checked) }
- .padding(vertical = 8.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(label, Modifier.weight(1f))
- Switch(checked, onCheckedChange)
+ Text("Export for Spreadsheet (CSV)", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
+ Text("Export your item list as a CSV file to open in Excel or Google Sheets.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
+
+ Button(onClick = onExportCsv, modifier = Modifier.fillMaxWidth(), enabled = hasPermission) {
+ Icon(Icons.Default.Upload, null)
+ Spacer(Modifier.width(8.dp))
+ Text("Export Items to CSV")
+ }
}
}
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun CustomThemeDialog(
- currentTheme: CustomTheme,
- onDismiss: () -> Unit,
- onConfirm: (CustomTheme) -> Unit
+private fun CustomThemeDialog(
+ currentTheme: CustomTheme, onDismiss: () -> Unit, onConfirm: (CustomTheme) -> Unit
) {
var theme by remember { mutableStateOf(currentTheme) }
var fontSizeScale by remember { mutableFloatStateOf(currentTheme.fontSizeScale) }
val colorOptions = listOf(
- "Red" to Color(0xFFE53E3E),
- "Orange" to Color(0xFFDD6B20),
- "Yellow" to Color(0xFFD69E2E),
- "Green" to Color(0xFF38A169),
- "Teal" to Color(0xFF319795),
- "Blue" to Color(0xFF3182CE),
- "Purple" to Color(0xFF805AD5),
- "Pink" to Color(0xFFD53F8C)
+ "Red" to Color(0xFFE53E3E), "Orange" to Color(0xFFDD6B20),
+ "Yellow" to Color(0xFFD69E2E), "Green" to Color(0xFF38A169),
+ "Teal" to Color(0xFF319795), "Blue" to Color(0xFF3182CE),
+ "Purple" to Color(0xFF805AD5), "Pink" to Color(0xFFD53F8C)
)
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("🎨 Custom Theme") },
text = {
- LazyColumn(
- verticalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- item {
- Text("Select Primary Color", fontWeight = FontWeight.Bold)
- }
+ LazyColumn(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ item { Text("Select Primary Color", fontWeight = FontWeight.Bold) }
item {
colorOptions.chunked(4).forEach { rowColors ->
- Row(
- Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- rowColors.forEach { (name, color) ->
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ rowColors.forEach { (_, color) ->
Box(
- Modifier
- .weight(1f)
- .aspectRatio(1f)
- .clip(RoundedCornerShape(12.dp))
- .background(color)
- .clickable {
- theme = theme.copy(
- primaryColor = color.toArgb().toLong()
- )
- },
+ Modifier.weight(1f).aspectRatio(1f).clip(RoundedCornerShape(12.dp))
+ .background(color).clickable { theme = theme.copy(primaryColor = color.toArgb().toLong()) },
contentAlignment = Alignment.Center
) {
if (Color(theme.primaryColor.toULong()) == color) {
- Icon(
- Icons.Default.Check,
- null,
- tint = Color.White,
- modifier = Modifier.size(24.dp)
- )
+ Icon(Icons.Default.Check, null, tint = Color.White, modifier = Modifier.size(24.dp))
}
}
}
@@ -1213,144 +545,128 @@ fun CustomThemeDialog(
Spacer(Modifier.height(8.dp))
}
}
-
item { HorizontalDivider(Modifier.padding(vertical = 8.dp)) }
-
+ item { Text("Select Background", fontWeight = FontWeight.Bold) }
item {
- Text("Select Background", fontWeight = FontWeight.Bold)
- }
- item {
- Row(
- Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- listOf(
- "Light" to Color(0xFFFFFFFF),
- "Dark" to Color(0xFF1A202C)
- ).forEach { (name, color) ->
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ listOf("Light" to Color(0xFFFFFFFF), "Dark" to Color(0xFF1A202C)).forEach { (name, color) ->
Box(
- Modifier
- .weight(1f)
- .height(60.dp)
- .clip(RoundedCornerShape(12.dp))
- .background(color)
- .clickable {
- theme = theme.copy(
- backgroundColor = color.toArgb().toLong()
- )
- },
+ Modifier.weight(1f).height(60.dp).clip(RoundedCornerShape(12.dp))
+ .background(color).clickable { theme = theme.copy(backgroundColor = color.toArgb().toLong()) },
contentAlignment = Alignment.Center
) {
- Text(
- name,
- color = if (color == Color.White) Color.Black else Color.White,
- fontWeight = if (Color(theme.backgroundColor.toULong()) == color)
- FontWeight.Bold else FontWeight.Normal
- )
+ Text(name, color = if (color == Color.White) Color.Black else Color.White,
+ fontWeight = if (Color(theme.backgroundColor.toULong()) == color) FontWeight.Bold else FontWeight.Normal)
}
}
}
}
-
item { HorizontalDivider(Modifier.padding(vertical = 8.dp)) }
-
- item {
- Text("Font & Icon Size Scale", fontWeight = FontWeight.Bold)
- }
+ item { Text("Font & Icon Size Scale", fontWeight = FontWeight.Bold) }
item {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Slider(
- value = fontSizeScale,
- onValueChange = { fontSizeScale = it },
- valueRange = 0.8f..1.5f,
- steps = 13,
- modifier = Modifier.fillMaxWidth()
- )
- Text(
- "Scale: %.2f (0.8x to 1.5x)".format(fontSizeScale),
- style = MaterialTheme.typography.bodySmall
+ value = fontSizeScale, onValueChange = { fontSizeScale = it }, valueRange = 0.8f..1.5f,
+ steps = 13, modifier = Modifier.fillMaxWidth()
)
+ Text(String.format(Locale.US, "Scale: %.2f (0.8x to 1.5x)", fontSizeScale), style = MaterialTheme.typography.bodySmall)
}
}
}
},
- confirmButton = {
- Button({
- onConfirm(theme.copy(fontSizeScale = fontSizeScale))
- }) { Text("Apply Theme") }
- },
- dismissButton = {
- TextButton(onDismiss) { Text("Cancel") }
- }
+ confirmButton = { Button({ onConfirm(theme.copy(fontSizeScale = fontSizeScale)) }) { Text("Apply Theme") } },
+ dismissButton = { TextButton(onDismiss) { Text("Cancel") } }
)
}
@Composable
-fun ProviderPriorityItem(
- name: String,
- priority: Int,
- onMoveUp: (() -> Unit)?,
- onMoveDown: (() -> Unit)?
+private fun ThemeSettingsContent(
+ settings: AppSettings, onThemeChange: (AppTheme) -> Unit, onFontSizeChange: (FontSize) -> Unit
) {
- Card(
- modifier = Modifier.fillMaxWidth(),
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.surfaceVariant
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Text("Select Theme", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold)
+ val themeItems = listOf(
+ "☀️ Light" to AppTheme.LIGHT, "🌙 Dark" to AppTheme.DARK, "🧛 Dracula" to AppTheme.DRACULA,
+ "🌊 Ocean" to AppTheme.OCEAN, "🌲 Forest" to AppTheme.FOREST, "🌅 Sunset" to AppTheme.SUNSET
)
- ) {
- Row(
- Modifier
- .fillMaxWidth()
- .padding(12.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Box(
- Modifier
- .size(36.dp)
- .clip(CircleShape)
- .background(MaterialTheme.colorScheme.primary),
- contentAlignment = Alignment.Center
- ) {
- Text(
- priority.toString(),
- color = MaterialTheme.colorScheme.onPrimary,
- fontWeight = FontWeight.Bold,
- fontSize = 16.sp
+ themeItems.chunked(2).forEach { rowItems ->
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ rowItems.forEach { (name, theme) ->
+ ThemePresetCard(
+ name = name, color = getThemeColor(theme), isSelected = settings.theme == theme,
+ onClick = { onThemeChange(theme) }, modifier = Modifier.weight(1f)
)
}
- Spacer(Modifier.width(12.dp))
- Text(name, fontWeight = FontWeight.Medium)
+ if (rowItems.size == 1) Spacer(modifier = Modifier.weight(1f))
}
- Row {
- IconButton(
- onClick = { onMoveUp?.invoke() },
- enabled = onMoveUp != null
- ) {
- Icon(
- Icons.Default.ArrowUpward,
- "Move Up",
- tint = if (onMoveUp != null)
- MaterialTheme.colorScheme.primary
- else
- Color.Gray
- )
- }
- IconButton(
- onClick = { onMoveDown?.invoke() },
- enabled = onMoveDown != null
- ) {
- Icon(
- Icons.Default.ArrowDownward,
- "Move Down",
- tint = if (onMoveDown != null)
- MaterialTheme.colorScheme.primary
- else
- Color.Gray
- )
+ }
+ HorizontalDivider(Modifier.padding(vertical = 12.dp))
+ Text("Font Size", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold)
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ FontSize.entries.forEach { size ->
+ FontSizeButton(size.name.lowercase().replaceFirstChar { it.uppercase() }, settings.fontSize == size) { onFontSizeChange(size) }
+ }
+ }
+ Text("Current size: ${settings.fontSize.name} (${String.format(Locale.US, "%.2f", settings.fontSize.scale)}x)",
+ style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 8.dp)
+ )
+ }
+}
+
+private fun getThemeColor(theme: AppTheme): Color {
+ return when (theme) {
+ AppTheme.LIGHT -> Color(0xFFE3F2FD)
+ AppTheme.DARK -> Color(0xFF212121)
+ AppTheme.DRACULA -> Color(0xFFBD93F9)
+ AppTheme.OCEAN -> Color(0xFF00B4D8)
+ AppTheme.FOREST -> Color(0xFF2D6A4F)
+ AppTheme.SUNSET -> Color(0xFFFF6B35)
+ AppTheme.VAMPIRE -> Color(0xFFFF1493)
+ AppTheme.CYBERPUNK -> Color(0xFFFF006E)
+ AppTheme.NEON -> Color(0xFF39FF14)
+ AppTheme.SYSTEM -> Color(0xFF808080)
+ AppTheme.CUSTOM -> Color(0xFF6200EE)
+ }
+}
+
+@Composable
+private fun ThemePresetCard(
+ name: String, color: Color, isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier.height(60.dp).clickable(onClick = onClick),
+ colors = CardDefaults.cardColors(containerColor = color.copy(alpha = 0.2f)),
+ border = if (isSelected) BorderStroke(3.dp, color) else null
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize().background(Brush.linearGradient(listOf(color.copy(0.4f), color.copy(0.1f)))),
+ contentAlignment = Alignment.Center
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(name, color = if (themeIsDark(color)) Color.White else Color.Black, fontWeight = FontWeight.Bold, fontSize = 13.sp)
+ if (isSelected) {
+ Spacer(Modifier.width(4.dp))
+ Icon(Icons.Default.Check, null, tint = color, modifier = Modifier.size(14.dp))
}
}
}
}
+}
+
+private fun themeIsDark(color: Color): Boolean {
+ val darkness = 1 - (0.299 * color.red + 0.587 * color.green + 0.114 * color.blue)
+ return darkness >= 0.5
+}
+
+@Composable
+private fun RowScope.FontSizeButton(text: String, selected: Boolean, onClick: () -> Unit) {
+ Button(
+ onClick, Modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
+ contentColor = if (selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ ) {
+ Text(text, fontSize = 11.sp, maxLines = 1)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/ShareScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/ShareScreen.kt
new file mode 100644
index 0000000..b6c11ba
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/ShareScreen.kt
@@ -0,0 +1,1047 @@
+@file:SuppressLint("UnusedBoxWithConstraintsScope")
+
+package com.samuel.inventorymanager.screens
+
+import android.annotation.SuppressLint
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.widget.Toast
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.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.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.ChevronRight
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Download
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Email
+import androidx.compose.material.icons.filled.History
+import androidx.compose.material.icons.filled.Link
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material.icons.filled.QrCode2
+import androidx.compose.material.icons.filled.Schedule
+import androidx.compose.material.icons.filled.Share
+import androidx.compose.material.icons.filled.Upload
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.Warning
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+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.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.google.firebase.database.DataSnapshot
+import com.google.firebase.database.DatabaseError
+import com.google.firebase.database.DatabaseReference
+import com.google.firebase.database.FirebaseDatabase
+import com.google.firebase.database.ValueEventListener
+import com.google.firebase.database.ktx.database
+import com.google.firebase.ktx.Firebase
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import com.google.zxing.BarcodeFormat
+import com.google.zxing.EncodeHintType
+import com.google.zxing.qrcode.QRCodeWriter
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.tasks.await
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import kotlin.random.Random
+
+// --- DATA CLASSES FOR SHARING ---
+
+data class ShareData(
+ val code: String = "",
+ val data: String = "",
+ val createdAt: Long = 0,
+ val expiresAt: Long = 0,
+ val creatorName: String = "Anonymous"
+)
+
+enum class ShareSection {
+ RECEIVED,
+ SHARED,
+}
+
+data class SharedGarageData(
+ val code: String,
+ val name: String,
+ val owner: String,
+ val sharedAt: Long,
+ val itemCount: Int,
+ val data: AppData // The full inventory data
+)
+
+data class ActiveShare(
+ val code: String,
+ val name: String,
+ val createdAt: Long,
+ val expiresAt: Long,
+ val itemCount: Int
+)
+
+data class UserProfile(
+ var username: String = "User",
+ var email: String = "",
+ var sharesCount: Int = 0,
+ var receivedCount: Int = 0
+)
+
+data class ShareHistoryItem(
+ val code: String,
+ val timestamp: Long,
+ val type: String, // "GENERATED" or "IMPORTED"
+ val itemCount: Int
+)
+
+enum class ShareExpiry(val label: String, val milliseconds: Long) {
+ MINUTES_30("30 Minutes", 30 * 60 * 1000L),
+ HOURS_24("24 Hours", 24 * 60 * 60 * 1000L),
+ DAYS_7("7 Days", 7 * 24 * 60 * 60 * 1000L),
+ DAYS_30("30 Days", 30 * 24 * 60 * 60 * 1000L)
+}
+
+
+// --- MAIN COMPOSABLE ---
+
+@Composable
+fun ShareScreen(
+ garages: List,
+ items: List
- ,
+ history: List,
+ onDataImported: (AppData) -> Unit
+) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val database: DatabaseReference = remember { Firebase.database.reference }
+
+ var selectedSection by remember { mutableStateOf(ShareSection.RECEIVED) }
+ var isLoading by remember { mutableStateOf(false) }
+ var statusMessage by remember { mutableStateOf("") }
+ var generatedCode by remember { mutableStateOf(null) }
+ var qrBitmap by remember { mutableStateOf(null) }
+ var importCode by remember { mutableStateOf("") }
+ var shareExpiry by remember { mutableStateOf(ShareExpiry.DAYS_7) }
+ var shareHistory by remember { mutableStateOf
>(emptyList()) }
+ var showExpiryDialog by remember { mutableStateOf(false) }
+ var sharedGarages by remember { mutableStateOf>(emptyList()) }
+ var myActiveShares by remember { mutableStateOf>(emptyList()) }
+ var userProfile by remember { mutableStateOf(UserProfile()) }
+ var showProfileDialog by remember { mutableStateOf(false) }
+
+ LaunchedEffect(Unit) {
+ // Load local data on start
+ shareHistory = loadShareHistory(context)
+ myActiveShares = loadMyActiveShares(context)
+ sharedGarages = loadSharedGarages(context)
+ userProfile = loadUserProfile(context)
+ }
+
+ LaunchedEffect(statusMessage) {
+ if (statusMessage.isNotEmpty()) {
+ delay(3000)
+ statusMessage = ""
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(androidx.compose.ui.graphics.Color(0xFF0F172A))
+ ) {
+ Column(modifier = Modifier.fillMaxSize()) {
+ ShareHeader(
+ onProfileClick = { showProfileDialog = true }
+ )
+
+ // Section Tabs
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ SectionTab(
+ icon = Icons.Default.Download,
+ title = "Received",
+ count = sharedGarages.size,
+ color = androidx.compose.ui.graphics.Color(0xFF10B981),
+ isSelected = selectedSection == ShareSection.RECEIVED,
+ onClick = { selectedSection = ShareSection.RECEIVED },
+ modifier = Modifier.weight(1f)
+ )
+ SectionTab(
+ icon = Icons.Default.Upload,
+ title = "My Shares",
+ count = myActiveShares.size,
+ color = androidx.compose.ui.graphics.Color(0xFF8B5CF6),
+ isSelected = selectedSection == ShareSection.SHARED,
+ onClick = { selectedSection = ShareSection.SHARED },
+ modifier = Modifier.weight(1f)
+ )
+ }
+
+ // Content Area
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .weight(1f)
+ ) {
+ when (selectedSection) {
+ ShareSection.RECEIVED -> ReceivedSection(
+ sharedGarages = sharedGarages,
+ importCode = importCode,
+ isLoading = isLoading,
+ onCodeChange = { importCode = it.uppercase().trim() },
+ onImport = {
+ if (importCode.isBlank()) {
+ statusMessage = "⚠️ Code cannot be empty"
+ return@ReceivedSection
+ }
+ scope.launch {
+ isLoading = true
+ try {
+ val snapshot: DataSnapshot = database.child("shares").child(importCode).get().await()
+ val shareData = snapshot.getValue(ShareData::class.java)
+ if (shareData != null) {
+ if (System.currentTimeMillis() > shareData.expiresAt) {
+ statusMessage = "⚠️ Code has expired"
+ } else {
+ val appData = Gson().fromJson(shareData.data, AppData::class.java)
+
+ val newSharedGarage = SharedGarageData(
+ code = importCode,
+ name = "${shareData.creatorName}'s Garage",
+ owner = shareData.creatorName,
+ sharedAt = System.currentTimeMillis(),
+ itemCount = appData.items.size,
+ data = appData
+ )
+ // Prevent duplicates
+ sharedGarages = (listOf(newSharedGarage) + sharedGarages).distinctBy { it.code }
+ saveSharedGarages(context, sharedGarages)
+
+ val historyItem = ShareHistoryItem(code = importCode, timestamp = System.currentTimeMillis(), type = "IMPORTED", itemCount = appData.items.size)
+ shareHistory = (listOf(historyItem) + shareHistory).distinctBy { it.code }
+ saveShareHistory(context, shareHistory)
+
+ userProfile.receivedCount = sharedGarages.size
+ saveUserProfile(context, userProfile)
+
+ statusMessage = "✅ Added to Received Garages!"
+ importCode = ""
+ }
+ } else {
+ statusMessage = "❌ Invalid code"
+ }
+ } catch (e: Exception) {
+ statusMessage = "❌ Import failed: ${e.message}"
+ } finally {
+ isLoading = false
+ }
+ }
+ },
+ onViewGarage = { sharedGarage ->
+ Toast.makeText(context, "Viewing ${sharedGarage.name}. \n(Functionality to be implemented)", Toast.LENGTH_SHORT).show()
+ },
+ onImportGarage = { sharedGarage ->
+ onDataImported(sharedGarage.data)
+ statusMessage = "✅ Imported to your main inventory!"
+ },
+ onDeleteGarage = { sharedGarage ->
+ sharedGarages = sharedGarages.filter { it.code != sharedGarage.code }
+ saveSharedGarages(context, sharedGarages)
+ userProfile.receivedCount = sharedGarages.size
+ saveUserProfile(context, userProfile)
+ statusMessage = "🗑️ Removed from received list"
+ }
+ )
+
+ ShareSection.SHARED -> MySharesSection(
+ garages = garages,
+ items = items,
+ history = history,
+ myActiveShares = myActiveShares,
+ generatedCode = generatedCode,
+ qrBitmap = qrBitmap,
+ shareExpiry = shareExpiry,
+ isLoading = isLoading,
+ onExpiryClick = { showExpiryDialog = true },
+ onGenerate = {
+ if (items.isEmpty()){
+ statusMessage = "⚠️ Cannot share an empty inventory"
+ return@MySharesSection
+ }
+ scope.launch {
+ isLoading = true
+ try {
+ val code = generateShareCode()
+ val appData = AppData(garages, items, history)
+ val shareData = ShareData(
+ code = code,
+ data = Gson().toJson(appData),
+ createdAt = System.currentTimeMillis(),
+ expiresAt = System.currentTimeMillis() + shareExpiry.milliseconds,
+ creatorName = userProfile.username
+ )
+ database.child("shares").child(code).setValue(shareData).await()
+ generatedCode = code
+ qrBitmap = generateQRCode(code)
+
+ val activeShare = ActiveShare(code = code, name = "My Inventory Share", createdAt = System.currentTimeMillis(), expiresAt = shareData.expiresAt, itemCount = items.size)
+ myActiveShares = (listOf(activeShare) + myActiveShares).distinctBy { it.code }
+ saveMyActiveShares(context, myActiveShares)
+
+ val historyItem = ShareHistoryItem(code = code, timestamp = System.currentTimeMillis(), type = "GENERATED", itemCount = items.size)
+ shareHistory = (listOf(historyItem) + shareHistory).distinctBy { it.code }
+ saveShareHistory(context, shareHistory)
+
+ userProfile.sharesCount = myActiveShares.size
+ saveUserProfile(context, userProfile)
+
+ statusMessage = "✅ Share code created successfully!"
+ } catch (e: Exception) {
+ statusMessage = "❌ Generation failed: ${e.message}"
+ } finally {
+ isLoading = false
+ }
+ }
+ },
+ onCopyCode = { code ->
+ copyToClipboard(context, code)
+ statusMessage = "📋 Copied code to clipboard!"
+ },
+ onCopyLink = { code ->
+ val link = "inventorymanager://share/$code"
+ copyToClipboard(context, link)
+ statusMessage = "🔗 Copied share link!"
+ },
+ onDeleteShare = { activeShare ->
+ myActiveShares = myActiveShares.filter { it.code != activeShare.code }
+ saveMyActiveShares(context, myActiveShares)
+ userProfile.sharesCount = myActiveShares.size
+ saveUserProfile(context, userProfile)
+
+ // Also delete from Firebase
+ database.child("shares").child(activeShare.code).removeValue()
+
+ statusMessage = "🗑️ Share code deleted"
+ }
+ )
+ }
+
+ // --- Status Message Display ---
+ this@Column.AnimatedVisibility(
+ visible = statusMessage.isNotEmpty(),
+ enter = fadeIn() + slideInVertically { it },
+ exit = fadeOut() + slideOutVertically { it },
+ modifier = Modifier.align(Alignment.BottomCenter).padding(20.dp)
+ ) {
+ Card(
+ colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color(0xFF1E293B)),
+ shape = RoundedCornerShape(16.dp),
+ elevation = CardDefaults.cardElevation(8.dp)
+ ) {
+ Text(
+ statusMessage,
+ modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
+ color = androidx.compose.ui.graphics.Color.White,
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+ }
+ }
+
+ if (isLoading) {
+ Box(
+ modifier = Modifier.fillMaxSize().background(androidx.compose.ui.graphics.Color.Black.copy(alpha = 0.7f)),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(
+ color = androidx.compose.ui.graphics.Color(0xFF8B5CF6),
+ modifier = Modifier.size(64.dp)
+ )
+ }
+ }
+ }
+
+ // --- Dialogs ---
+
+ if (showExpiryDialog) {
+ AlertDialog(
+ onDismissRequest = { showExpiryDialog = false },
+ icon = { Icon(Icons.Default.Schedule, null) },
+ title = { Text("Set Share Expiry") },
+ text = {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ ShareExpiry.entries.forEach { expiry ->
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ shareExpiry = expiry
+ showExpiryDialog = false
+ }
+ .padding(12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(expiry.label)
+ if (shareExpiry == expiry) {
+ Icon(Icons.Default.Check, null, tint = androidx.compose.ui.graphics.Color(0xFF10B981))
+ }
+ }
+ }
+ }
+ },
+ confirmButton = { TextButton(onClick = { showExpiryDialog = false }) { Text("Close") } }
+ )
+ }
+
+ if (showProfileDialog) {
+ ProfileDialog(
+ userProfile = userProfile,
+ onDismiss = { showProfileDialog = false },
+ onSave = { newProfile ->
+ userProfile = newProfile
+ saveUserProfile(context, newProfile)
+ showProfileDialog = false
+ statusMessage = "✅ Profile updated!"
+ }
+ )
+ }
+}
+
+
+// --- HELPER COMPOSABLES & FUNCTIONS ---
+
+@Composable
+private fun ShareHeader(onProfileClick: () -> Unit) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(140.dp)
+ .background(Brush.verticalGradient(colors = listOf(androidx.compose.ui.graphics.Color(0xFF6366F1), androidx.compose.ui.graphics.Color(0xFF8B5CF6))))
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 24.dp, vertical = 20.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
+ Surface(shape = CircleShape, color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.2f), modifier = Modifier.size(60.dp)) {
+ Box(contentAlignment = Alignment.Center) {
+ Icon(Icons.Default.Share, null, modifier = Modifier.size(32.dp), tint = androidx.compose.ui.graphics.Color.White)
+ }
+ }
+ Column {
+ Text("Share Hub", color = androidx.compose.ui.graphics.Color.White, fontSize = 28.sp, fontWeight = FontWeight.ExtraBold)
+ Text("Manage Your Network", color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.9f), fontSize = 14.sp)
+ }
+ }
+ IconButton(
+ onClick = onProfileClick,
+ modifier = Modifier
+ .size(48.dp)
+ .background(androidx.compose.ui.graphics.Color.White.copy(alpha = 0.2f), CircleShape)
+ ) {
+ Icon(Icons.Default.AccountCircle, "Profile", tint = androidx.compose.ui.graphics.Color.White, modifier = Modifier.size(28.dp))
+ }
+ }
+ }
+}
+
+
+@Composable
+private fun ProfileDialog(
+ userProfile: UserProfile,
+ onDismiss: () -> Unit,
+ onSave: (UserProfile) -> Unit
+) {
+ var username by remember { mutableStateOf(userProfile.username) }
+ var email by remember { mutableStateOf(userProfile.email) }
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ icon = { Icon(Icons.Default.AccountCircle, null, modifier = Modifier.size(48.dp)) },
+ title = { Text("Your Profile", fontSize = 24.sp, fontWeight = FontWeight.Bold) },
+ text = {
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ OutlinedTextField(
+ value = username,
+ onValueChange = { username = it },
+ label = { Text("Username") },
+ leadingIcon = { Icon(Icons.Default.Person, null) },
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true
+ )
+ OutlinedTextField(
+ value = email,
+ onValueChange = { email = it },
+ label = { Text("Email (Optional)") },
+ leadingIcon = { Icon(Icons.Default.Email, null) },
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
+ )
+
+ HorizontalDivider()
+
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(userProfile.sharesCount.toString(), fontSize = 24.sp, fontWeight = FontWeight.Bold, color = androidx.compose.ui.graphics.Color(0xFF8B5CF6))
+ Text("Shares Created", fontSize = 12.sp)
+ }
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(userProfile.receivedCount.toString(), fontSize = 24.sp, fontWeight = FontWeight.Bold, color = androidx.compose.ui.graphics.Color(0xFF10B981))
+ Text("Shares Received", fontSize = 12.sp)
+ }
+ }
+ }
+ },
+ confirmButton = { Button(onClick = { onSave(UserProfile(username, email, userProfile.sharesCount, userProfile.receivedCount)) }) { Text("Save") } },
+ dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } }
+ )
+}
+
+@Composable
+private fun SectionTab(
+ icon: ImageVector,
+ title: String,
+ count: Int,
+ color: androidx.compose.ui.graphics.Color,
+ isSelected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ onClick = onClick,
+ modifier = modifier.height(90.dp),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(containerColor = if (isSelected) color.copy(alpha = 0.2f) else androidx.compose.ui.graphics.Color(0xFF1E293B)),
+ border = if (isSelected) BorderStroke(2.dp, color) else null,
+ elevation = CardDefaults.cardElevation(if (isSelected) 8.dp else 2.dp)
+ ) {
+ Column(
+ modifier = Modifier.fillMaxSize().padding(12.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(icon, contentDescription = null, tint = if (isSelected) color else androidx.compose.ui.graphics.Color.White.copy(alpha = 0.6f), modifier = Modifier.size(28.dp))
+ Spacer(Modifier.height(6.dp))
+ Text(title, color = androidx.compose.ui.graphics.Color.White, fontSize = 14.sp, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal)
+ Text("$count total", color = if (isSelected) color else androidx.compose.ui.graphics.Color.White.copy(alpha = 0.5f), fontSize = 11.sp, fontWeight = FontWeight.Bold)
+ }
+ }
+}
+
+@Composable
+private fun ReceivedSection(
+ sharedGarages: List,
+ importCode: String,
+ isLoading: Boolean,
+ onCodeChange: (String) -> Unit,
+ onImport: () -> Unit,
+ onViewGarage: (SharedGarageData) -> Unit,
+ onImportGarage: (SharedGarageData) -> Unit,
+ onDeleteGarage: (SharedGarageData) -> Unit
+) {
+ Column(
+ modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ // Import Card
+ Card(
+ colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color(0xFF1E293B)),
+ shape = RoundedCornerShape(20.dp),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Column(modifier = Modifier.fillMaxWidth().padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ Surface(shape = CircleShape, color = androidx.compose.ui.graphics.Color(0xFF10B981).copy(alpha = 0.2f), modifier = Modifier.size(48.dp)) {
+ Box(contentAlignment = Alignment.Center) {
+ Icon(Icons.Default.Add, null, tint = androidx.compose.ui.graphics.Color(0xFF10B981), modifier = Modifier.size(24.dp))
+ }
+ }
+ Column {
+ Text("Import Share Code", color = androidx.compose.ui.graphics.Color.White, fontSize = 20.sp, fontWeight = FontWeight.Bold)
+ Text("Add someone's garage to your list", color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.6f), fontSize = 13.sp)
+ }
+ }
+
+ OutlinedTextField(
+ value = importCode,
+ onValueChange = onCodeChange,
+ label = { Text("Enter Code") },
+ placeholder = { Text("ABC-123") },
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true,
+ shape = RoundedCornerShape(12.dp),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = androidx.compose.ui.graphics.Color(0xFF10B981),
+ unfocusedBorderColor = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.3f),
+ cursorColor = androidx.compose.ui.graphics.Color(0xFF10B981),
+ focusedLabelColor = androidx.compose.ui.graphics.Color(0xFF10B981),
+ unfocusedLabelColor = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.5f),
+ focusedTextColor = androidx.compose.ui.graphics.Color.White,
+ unfocusedTextColor = androidx.compose.ui.graphics.Color.White
+ )
+ )
+
+ Button(
+ onClick = onImport,
+ modifier = Modifier.fillMaxWidth().height(52.dp),
+ colors = ButtonDefaults.buttonColors(containerColor = androidx.compose.ui.graphics.Color(0xFF10B981)),
+ shape = RoundedCornerShape(12.dp),
+ enabled = !isLoading && importCode.isNotBlank()
+ ) {
+ Icon(Icons.Default.Download, null, modifier = Modifier.size(20.dp))
+ Spacer(Modifier.width(10.dp))
+ Text("Add to Received", fontSize = 15.sp, fontWeight = FontWeight.Bold)
+ }
+ }
+ }
+
+ // Received Garages List
+ if (sharedGarages.isNotEmpty()){
+ Text(
+ "Received Garages (${sharedGarages.size})",
+ color = androidx.compose.ui.graphics.Color.White,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(top = 8.dp)
+ )
+ }
+
+ if (sharedGarages.isEmpty()) {
+ Card(colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color(0xFF1E293B)), shape = RoundedCornerShape(20.dp)) {
+ Column(
+ modifier = Modifier.fillMaxWidth().padding(40.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Icon(Icons.Default.Download, null, modifier = Modifier.size(56.dp), tint = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.2f))
+ Text("No Garages Yet", color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.5f), fontSize = 16.sp, fontWeight = FontWeight.Medium)
+ Text("Import a code above to get started", color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.3f), fontSize = 13.sp, textAlign = TextAlign.Center)
+ }
+ }
+ } else {
+ sharedGarages.forEach { sharedGarage ->
+ ReceivedGarageCard(
+ sharedGarage = sharedGarage,
+ onView = { onViewGarage(sharedGarage) },
+ onImport = { onImportGarage(sharedGarage) },
+ onDelete = { onDeleteGarage(sharedGarage) }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun ReceivedGarageCard(
+ sharedGarage: SharedGarageData,
+ onView: () -> Unit,
+ onImport: () -> Unit,
+ onDelete: () -> Unit
+) {
+ Card(
+ colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color(0xFF1E293B)),
+ shape = RoundedCornerShape(20.dp),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Column(modifier = Modifier.fillMaxWidth().padding(20.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) {
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Top) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(sharedGarage.name, color = androidx.compose.ui.graphics.Color.White, fontSize = 19.sp, fontWeight = FontWeight.Bold)
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Default.Person, null, tint = androidx.compose.ui.graphics.Color(0xFF10B981), modifier = Modifier.size(16.dp))
+ Text("By ${sharedGarage.owner}", color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.7f), fontSize = 13.sp)
+ }
+ }
+ Column(horizontalAlignment = Alignment.End) {
+ Text(formatTimestamp(sharedGarage.sharedAt), color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.4f), fontSize = 11.sp)
+ IconButton(onClick = onDelete, modifier = Modifier.size(32.dp)) {
+ Icon(Icons.Default.Delete, null, tint = androidx.compose.ui.graphics.Color(0xFFEF4444), modifier = Modifier.size(20.dp))
+ }
+ }
+ }
+
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ StatPill("Items", sharedGarage.itemCount.toString(), androidx.compose.ui.graphics.Color(0xFF8B5CF6))
+ StatPill("Garages", sharedGarage.data.garages.size.toString(), androidx.compose.ui.graphics.Color(0xFF3B82F6))
+ StatPill("Logs", sharedGarage.data.history.size.toString(), androidx.compose.ui.graphics.Color(0xFFF59E0B))
+ }
+
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ OutlinedButton(
+ onClick = onView,
+ modifier = Modifier.weight(1f).height(48.dp),
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.outlinedButtonColors(contentColor = androidx.compose.ui.graphics.Color(0xFF3B82F6)),
+ border = BorderStroke(2.dp, androidx.compose.ui.graphics.Color(0xFF3B82F6))
+ ) {
+ Icon(Icons.Default.Visibility, null, modifier = Modifier.size(18.dp))
+ Spacer(Modifier.width(6.dp))
+ Text("View", fontWeight = FontWeight.Bold)
+ }
+ Button(
+ onClick = onImport,
+ modifier = Modifier.weight(1f).height(48.dp),
+ colors = ButtonDefaults.buttonColors(containerColor = androidx.compose.ui.graphics.Color(0xFF10B981)),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Icon(Icons.Default.Download, null, modifier = Modifier.size(18.dp))
+ Spacer(Modifier.width(6.dp))
+ Text("Import All", fontWeight = FontWeight.Bold)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun StatPill(label: String, value: String, color: androidx.compose.ui.graphics.Color) {
+ Surface(shape = RoundedCornerShape(10.dp), color = color.copy(alpha = 0.15f)) {
+ Row(
+ modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(value, color = color, fontSize = 16.sp, fontWeight = FontWeight.Bold)
+ Text(label, color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.6f), fontSize = 12.sp)
+ }
+ }
+}
+
+@Composable
+private fun MySharesSection(
+ garages: List,
+ items: List- ,
+ history: List,
+ myActiveShares: List,
+ generatedCode: String?,
+ qrBitmap: Bitmap?,
+ shareExpiry: ShareExpiry,
+ isLoading: Boolean,
+ onExpiryClick: () -> Unit,
+ onGenerate: () -> Unit,
+ onCopyCode: (String) -> Unit,
+ onCopyLink: (String) -> Unit,
+ onDeleteShare: (ActiveShare) -> Unit
+) {
+ Column(
+ modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ // Current Inventory Stats
+ Card(
+ colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color(0xFF1E293B)),
+ shape = RoundedCornerShape(20.dp),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Column(modifier = Modifier.fillMaxWidth().padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ Surface(shape = CircleShape, color = androidx.compose.ui.graphics.Color(0xFF8B5CF6).copy(alpha = 0.2f), modifier = Modifier.size(48.dp)) {
+ Box(contentAlignment = Alignment.Center) {
+ Icon(Icons.Default.Upload, null, tint = androidx.compose.ui.graphics.Color(0xFF8B5CF6), modifier = Modifier.size(24.dp))
+ }
+ }
+ Column {
+ Text("Your Inventory", color = androidx.compose.ui.graphics.Color.White, fontSize = 20.sp, fontWeight = FontWeight.Bold)
+ Text("Ready to share with others", color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.6f), fontSize = 13.sp)
+ }
+ }
+
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ StatPill("Garages", garages.size.toString(), androidx.compose.ui.graphics.Color(0xFF3B82F6))
+ StatPill("Items", items.size.toString(), androidx.compose.ui.graphics.Color(0xFF8B5CF6))
+ StatPill("Logs", history.size.toString(), androidx.compose.ui.graphics.Color(0xFFF59E0B))
+ }
+ }
+ }
+
+ // Expiry Selection
+ Card(
+ colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color(0xFF1E293B)),
+ shape = RoundedCornerShape(20.dp),
+ modifier = Modifier.clickable { onExpiryClick() },
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Row(modifier = Modifier.fillMaxWidth().padding(20.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Default.Schedule, null, tint = androidx.compose.ui.graphics.Color(0xFFF59E0B), modifier = Modifier.size(24.dp))
+ Column {
+ Text("Expires In", color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.6f), fontSize = 13.sp)
+ Text(shareExpiry.label, color = androidx.compose.ui.graphics.Color.White, fontSize = 17.sp, fontWeight = FontWeight.Bold)
+ }
+ }
+ Icon(Icons.Default.ChevronRight, null, tint = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.3f), modifier = Modifier.size(28.dp))
+ }
+ }
+
+ // Generate Button
+ Button(
+ onClick = onGenerate,
+ modifier = Modifier.fillMaxWidth().height(56.dp),
+ colors = ButtonDefaults.buttonColors(containerColor = androidx.compose.ui.graphics.Color(0xFF8B5CF6)),
+ shape = RoundedCornerShape(16.dp),
+ enabled = !isLoading && items.isNotEmpty()
+ ) {
+ Icon(Icons.Default.QrCode2, null, modifier = Modifier.size(24.dp))
+ Spacer(Modifier.width(12.dp))
+ Text("Generate Share Code", fontSize = 16.sp, fontWeight = FontWeight.Bold)
+ }
+
+ // Generated Code Display
+ AnimatedVisibility(visible = generatedCode != null, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically()) {
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ if (qrBitmap != null) {
+ Card(
+ colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color.White),
+ shape = RoundedCornerShape(24.dp),
+ elevation = CardDefaults.cardElevation(8.dp)
+ ) {
+ Box(modifier = Modifier.fillMaxWidth().padding(24.dp), contentAlignment = Alignment.Center) {
+ Image(bitmap = qrBitmap.asImageBitmap(), contentDescription = "QR Code", modifier = Modifier.size(220.dp))
+ }
+ }
+ }
+
+ Card(
+ colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color(0xFF1E293B)),
+ shape = RoundedCornerShape(20.dp),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Column(
+ modifier = Modifier.fillMaxWidth().padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text("Your Share Code", color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.6f), fontSize = 13.sp)
+ Text(generatedCode ?: "", color = androidx.compose.ui.graphics.Color(0xFF8B5CF6), fontSize = 34.sp, fontWeight = FontWeight.ExtraBold, letterSpacing = 5.sp)
+ }
+ }
+
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ OutlinedButton(
+ onClick = { generatedCode?.let { onCopyCode(it) } },
+ modifier = Modifier.weight(1f).height(52.dp),
+ shape = RoundedCornerShape(14.dp),
+ colors = ButtonDefaults.outlinedButtonColors(contentColor = androidx.compose.ui.graphics.Color.White),
+ border = BorderStroke(2.dp, androidx.compose.ui.graphics.Color(0xFF8B5CF6))
+ ) {
+ Icon(Icons.Default.ContentCopy, null, modifier = Modifier.size(20.dp))
+ Spacer(Modifier.width(8.dp))
+ Text("Copy Code", fontWeight = FontWeight.Bold)
+ }
+ OutlinedButton(
+ onClick = { generatedCode?.let { onCopyLink(it) } },
+ modifier = Modifier.weight(1f).height(52.dp),
+ shape = RoundedCornerShape(14.dp),
+ colors = ButtonDefaults.outlinedButtonColors(contentColor = androidx.compose.ui.graphics.Color.White),
+ border = BorderStroke(2.dp, androidx.compose.ui.graphics.Color(0xFF3B82F6))
+ ) {
+ Icon(Icons.Default.Link, null, modifier = Modifier.size(20.dp))
+ Spacer(Modifier.width(8.dp))
+ Text("Copy Link", fontWeight = FontWeight.Bold)
+ }
+ }
+ }
+ }
+
+ // Active Shares List
+ if (myActiveShares.isNotEmpty()) {
+ Text(
+ "Active Shares (${myActiveShares.size})",
+ color = androidx.compose.ui.graphics.Color.White,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(top = 12.dp)
+ )
+ myActiveShares.forEach { share ->
+ ActiveShareCard(
+ activeShare = share,
+ onCopy = { onCopyCode(share.code) },
+ onDelete = { onDeleteShare(share) }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun ActiveShareCard(activeShare: ActiveShare, onCopy: () -> Unit, onDelete: () -> Unit) {
+ Card(
+ colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color(0xFF1E293B)),
+ shape = RoundedCornerShape(20.dp),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(20.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(activeShare.name, color = androidx.compose.ui.graphics.Color.White, fontSize = 17.sp, fontWeight = FontWeight.Bold)
+ Spacer(Modifier.height(4.dp))
+ Text(activeShare.code, color = androidx.compose.ui.graphics.Color(0xFF8B5CF6), fontSize = 20.sp, fontWeight = FontWeight.Bold, letterSpacing = 2.sp)
+ Spacer(Modifier.height(6.dp))
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
+ Text("${activeShare.itemCount} items", color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.5f), fontSize = 12.sp)
+ Text("•", color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.3f), fontSize = 12.sp)
+ Text("Expires ${formatTimestamp(activeShare.expiresAt, "MMM dd, hh:mm a")}", color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.5f), fontSize = 12.sp)
+ }
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
+ IconButton(onClick = onCopy, modifier = Modifier.size(44.dp)) {
+ Icon(Icons.Default.ContentCopy, null, tint = androidx.compose.ui.graphics.Color.White, modifier = Modifier.size(22.dp))
+ }
+ IconButton(onClick = onDelete, modifier = Modifier.size(44.dp)) {
+ Icon(Icons.Default.Delete, null, tint = androidx.compose.ui.graphics.Color(0xFFEF4444), modifier = Modifier.size(22.dp))
+ }
+ }
+ }
+ }
+}
+
+
+// --- UTILITY AND DATA PERSISTENCE ---
+
+private fun generateShareCode(): String {
+ val chars = ('A'..'Z').toList()
+ val nums = ('0'..'9').toList()
+ val part1 = (1..3).map { chars.random() }.joinToString("")
+ val part2 = (1..3).map { nums.random() }.joinToString("")
+ return "$part1-$part2"
+}
+
+private fun generateQRCode(text: String): Bitmap? {
+ return try {
+ val hints = mapOf(EncodeHintType.MARGIN to 1)
+ val writer = QRCodeWriter()
+ val bitMatrix = writer.encode(text, BarcodeFormat.QR_CODE, 512, 512, hints)
+ val width = bitMatrix.width
+ val height = bitMatrix.height
+ val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ bmp.setPixel(x, y, if (bitMatrix[x, y]) Color.BLACK else Color.WHITE)
+ }
+ }
+ bmp
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+}
+
+private fun formatTimestamp(timestamp: Long, pattern: String = "MMM dd, yyyy"): String {
+ val sdf = SimpleDateFormat(pattern, Locale.US)
+ return try {
+ "on ${sdf.format(Date(timestamp))}"
+ } catch (e: Exception) {
+ "Invalid Date"
+ }
+}
+
+private fun copyToClipboard(context: Context, text: String) {
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText("share_code", text)
+ clipboard.setPrimaryClip(clip)
+}
+
+
+// Generic file helpers for data persistence
+private inline fun saveDataToFile(context: Context, data: T, filename: String) {
+ val file = File(context.filesDir, filename)
+ file.writeText(Gson().toJson(data))
+}
+
+private inline fun loadDataFromFile(context: Context, filename: String): T? {
+ val file = File(context.filesDir, filename)
+ return if (file.exists()) {
+ val json = file.readText()
+ Gson().fromJson(json, object : TypeToken() {}.type)
+ } else {
+ null
+ }
+}
+
+// Specific implementations for each data type
+private fun saveUserProfile(context: Context, profile: UserProfile) = saveDataToFile(context, profile, "user_profile.json")
+private fun loadUserProfile(context: Context): UserProfile = loadDataFromFile(context, "user_profile.json") ?: UserProfile()
+
+private fun saveShareHistory(context: Context, history: List) = saveDataToFile(context, history, "share_history.json")
+private fun loadShareHistory(context: Context): List = loadDataFromFile
>(context, "share_history.json") ?: emptyList()
+
+private fun saveSharedGarages(context: Context, garages: List) = saveDataToFile(context, garages, "shared_garages.json")
+private fun loadSharedGarages(context: Context): List = loadDataFromFile>(context, "shared_garages.json") ?: emptyList()
+
+private fun saveMyActiveShares(context: Context, shares: List) = saveDataToFile(context, shares, "my_active_shares.json")
+private fun loadMyActiveShares(context: Context): List = loadDataFromFile>(context, "my_active_shares.json") ?: emptyList()
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/services/AIService.kt b/app/src/main/java/com/samuel/inventorymanager/services/AIService.kt
new file mode 100644
index 0000000..923a20f
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/services/AIService.kt
@@ -0,0 +1,890 @@
+package com.samuel.inventorymanager.services
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.net.Uri
+import android.util.Log
+import com.google.mlkit.vision.common.InputImage
+import com.google.mlkit.vision.label.ImageLabeling
+import com.google.mlkit.vision.label.defaults.ImageLabelerOptions
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.tasks.await
+import kotlinx.coroutines.withContext
+import kotlin.math.abs
+
+class AIService(private val context: Context) {
+
+ companion object {
+ private const val TAG = "AIService"
+ private const val LABEL_CONFIDENCE_THRESHOLD = 0.65f
+
+ // Non-English words to aggressively filter
+ private val NON_ENGLISH_INDICATORS = setOf(
+ "capacidad", "almacenamiento", "memoria", "unidad", "disco", "tarjeta",
+ "portátil", "externo", "rápido", "alta", "velocidad", "garantía",
+ "capacité", "stockage", "mémoire", "unité", "disque", "carte",
+ "portable", "externe", "rapide", "haute", "vitesse", "garantie",
+ "clé", "lecteur", "disponible", "producto", "produit"
+ )
+
+ private val ENGLISH_TECH_WORDS = setOf(
+ "storage", "memory", "drive", "flash", "card", "usb", "external",
+ "portable", "capacity", "speed", "ultra", "pro", "extreme", "plus",
+ "stick", "disk", "solid", "state", "high", "fast", "warranty",
+ "cruzer", "glide", "blade", "ultra", "fit", "edge", "force"
+ )
+
+ // Noise words that shouldn't be in product names
+ private val NOISE_WORDS = setOf(
+ "the", "with", "for", "and", "from", "this", "that", "these",
+ "capacity", "storage", "available", "made", "china", "usa",
+ "high", "speed", "fast", "quality", "compatible", "warranty"
+ )
+ }
+
+ private val labeler by lazy {
+ ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS)
+ }
+
+ private val ocrService = OCRService(context)
+ private val brandDatabase = BrandDatabase()
+ private val productTypeClassifier = ProductTypeClassifier()
+ private val contextualExtractor = ContextualExtractor()
+ private val languageDetector = LanguageDetector()
+ private val logoDetector = LogoDetector()
+ private val nameBuilder = IntelligentNameBuilder()
+
+ suspend fun analyzeItemFromBitmap(bitmap: Bitmap): AIAnalysisResult = withContext(Dispatchers.IO) {
+ try {
+ Log.d(TAG, "=== Starting Advanced AI Analysis ===")
+
+ val tempUri = saveBitmapToTempUri(bitmap)
+
+ // Step 1: OCR
+ val ocrResult = try {
+ ocrService.performOCR(tempUri)
+ } catch (e: Exception) {
+ Log.w(TAG, "OCR failed", e)
+ OCRResult("", 0.0, "ML Kit")
+ }
+
+ Log.d(TAG, "Raw OCR Text:\n${ocrResult.text}")
+
+ // Step 2: Image Labeling
+ val image = InputImage.fromBitmap(bitmap, 0)
+ val rawLabels = try {
+ labeler.process(image).await()
+ } catch (e: Exception) {
+ Log.w(TAG, "Image labeling failed", e)
+ emptyList()
+ }
+
+ val labels = rawLabels
+ .filter { it.confidence >= LABEL_CONFIDENCE_THRESHOLD }
+ .map { LabelInfo(it.text, it.confidence) }
+
+ Log.d(TAG, "Image Labels: ${labels.joinToString { "${it.label}(${it.confidence})" }}")
+
+ // Step 3: Logo Detection
+ val detectedLogos = logoDetector.detectLogos(ocrResult.text, labels)
+ Log.d(TAG, "Detected Logos: $detectedLogos")
+
+ // Step 4: Filter English text
+ val filteredText = filterEnglishText(ocrResult.text)
+ Log.d(TAG, "Filtered English Text:\n$filteredText")
+
+ // Step 5: Preprocess
+ val analysisContext = AnalysisContext(
+ rawText = filteredText,
+ imageLabels = labels,
+ lines = preprocessText(filteredText),
+ detectedLogos = detectedLogos
+ )
+
+ Log.d(TAG, "Preprocessed ${analysisContext.lines.size} semantic units")
+
+ // Step 6: Extract product information
+ val productInfo = extractProductInformation(analysisContext)
+ Log.d(TAG, "=== Extracted Product Info ===")
+ Log.d(TAG, "Brand: ${productInfo.brand}")
+ Log.d(TAG, "Product Line: ${productInfo.productLine}")
+ Log.d(TAG, "Model: ${productInfo.modelNumber}")
+ Log.d(TAG, "Capacity: ${productInfo.capacity}")
+ Log.d(TAG, "Category: ${productInfo.category}")
+
+ // Step 7: Build final result
+ val result = buildStructuredResult(productInfo, analysisContext)
+ Log.d(TAG, "=== Final Result ===")
+ Log.d(TAG, "Name: ${result.itemName}")
+ Log.d(TAG, "Confidence: ${result.confidence}")
+
+ cleanupTempFiles()
+ result
+
+ } catch (e: Exception) {
+ Log.e(TAG, "AI analysis failed", e)
+ throw Exception("AI analysis failed: ${e.message}")
+ }
+ }
+
+ private fun buildStructuredResult(
+ productInfo: ProductInfo,
+ context: AnalysisContext
+ ): AIAnalysisResult {
+ return AIAnalysisResult(
+ itemName = productInfo.name,
+ modelNumber = productInfo.modelNumber,
+ description = productInfo.description,
+ condition = productInfo.condition,
+ sizeCategory = null,
+ estimatedPrice = productInfo.price,
+ dimensions = productInfo.dimensions,
+ rawText = context.rawText.ifBlank { null },
+ confidence = calculateOverallConfidence(productInfo, context)
+ )
+ }
+
+ private fun filterEnglishText(rawText: String): String {
+ val lines = rawText.lines().map { it.trim() }.filter { it.isNotBlank() }
+ val scoredLines = mutableListOf>()
+
+ for (line in lines) {
+ val score = languageDetector.scoreEnglishLikelihood(line)
+ if (score >= 0.3) {
+ scoredLines.add(line to score)
+ Log.d(TAG, "✓ Line: '$line' | Score: $score")
+ } else {
+ Log.d(TAG, "✗ Filtered: '$line' | Score: $score")
+ }
+ }
+
+ val englishLines = scoredLines
+ .sortedByDescending { it.second }
+ .map { it.first }
+
+ return if (englishLines.isNotEmpty()) {
+ englishLines.joinToString("\n")
+ } else {
+ Log.w(TAG, "No English text detected, using original")
+ rawText
+ }
+ }
+
+ private fun preprocessText(rawText: String): List {
+ val lines = rawText.lines().map { it.trim() }.filter { it.isNotBlank() }
+ val semanticUnits = mutableListOf()
+ var position = 0
+
+ for (line in lines) {
+ val cleaned = cleanLine(line)
+ if (cleaned.isEmpty()) continue
+
+ val quality = assessLineQuality(cleaned)
+ if (quality.isUseful) {
+ semanticUnits.add(
+ SemanticUnit(
+ text = cleaned,
+ originalText = line,
+ position = position++,
+ quality = quality,
+ tokens = tokenize(cleaned),
+ type = classifyLineType(cleaned)
+ )
+ )
+ }
+ }
+
+ return groupRelatedUnits(semanticUnits)
+ }
+
+ private fun cleanLine(line: String): String {
+ var cleaned = line
+
+ // Filter lines with too many accents
+ val accentedCount = cleaned.count { it in "áéíóúñüàèìòùâêîôûëïöçÁÉÍÓÚÑÜÀÈÌÒÙÂÊÎÔÛËÏÖÇ" }
+ if (accentedCount > 2) {
+ return ""
+ }
+
+ cleaned = cleaned.replace(Regex("\\s+"), " ").trim()
+
+ // Noise patterns
+ val noisePatterns = listOf(
+ Regex("^(MSIP|REM|FCC|CE|UL|ETL|ROHS|WEEE|CE\\d+|RECYCLABLE).*", RegexOption.IGNORE_CASE),
+ Regex(".*\\b(COMPLIANCE|CERTIFIED|APPROVED|REGULATION)\\s+\\d+.*", RegexOption.IGNORE_CASE),
+ Regex("^\\d{13,}$"),
+ Regex("^[A-Z]{10,}$"),
+ Regex("^[0-9A-F]{16,}$"),
+ Regex("^www\\..*", RegexOption.IGNORE_CASE),
+ Regex(".*@.*\\.com", RegexOption.IGNORE_CASE)
+ )
+
+ for (pattern in noisePatterns) {
+ if (pattern.matches(cleaned)) return ""
+ }
+
+ // Filter obvious non-English
+ val lowerCleaned = cleaned.lowercase()
+ val hasNonEnglish = NON_ENGLISH_INDICATORS.any { lowerCleaned.contains(it) }
+ val hasEnglish = ENGLISH_TECH_WORDS.any { lowerCleaned.contains(it) }
+
+ if (hasNonEnglish && !hasEnglish) {
+ return ""
+ }
+
+ return cleaned
+ }
+
+ private fun assessLineQuality(line: String): LineQuality {
+ var score = 50.0
+
+ // Length scoring
+ score += when (line.length) {
+ in 3..5 -> -20.0
+ in 6..10 -> 5.0
+ in 11..40 -> 25.0
+ in 41..80 -> 15.0
+ else -> -25.0
+ }
+
+ // Character composition
+ val letters = line.count { it.isLetter() }
+ val numbers = line.count { it.isDigit() }
+ val total = letters + numbers
+
+ if (total > 0) {
+ val letterRatio = letters.toDouble() / total
+ score += when {
+ letterRatio in 0.4..0.8 -> 25.0
+ letterRatio in 0.2..0.9 -> 10.0
+ else -> -15.0
+ }
+ }
+
+ // Word count
+ val words = line.split(Regex("\\s+")).filter { it.length > 1 }
+ score += when (words.size) {
+ 0, 1 -> -10.0
+ 2, 3 -> 15.0
+ 4, 5, 6 -> 25.0
+ else -> 10.0
+ }
+
+ // Meaningful words
+ if (words.any { it.length >= 4 }) score += 20.0
+
+ // Special characters
+ val specialChars = line.count { !it.isLetterOrDigit() && !it.isWhitespace() }
+ val specialRatio = if (line.isNotEmpty()) specialChars.toDouble() / line.length else 0.0
+ if (specialRatio > 0.4) score -= 40.0
+
+ // Product keywords
+ val productKeywords = listOf(
+ "GB", "TB", "MB", "USB", "Drive", "Flash", "Storage", "Memory",
+ "External", "Portable", "SSD", "HDD", "Card", "Stick", "Pro",
+ "Ultra", "Elite", "Max", "Plus", "Extreme", "Cruzer", "Glide",
+ "Blade", "Edge", "Fit", "Force", "Evo", "Premium"
+ )
+
+ val keywordMatches = productKeywords.count { line.contains(it, ignoreCase = true) }
+ score += keywordMatches * 20.0
+
+ // English scoring
+ val englishScore = languageDetector.scoreEnglishLikelihood(line)
+ score += (englishScore * 35.0)
+
+ // Penalty for accents
+ val accentedChars = line.count { it in "áéíóúñüàèìòùâêîôûëïöç" }
+ score -= (accentedChars * 20.0)
+
+ return LineQuality(score, score > 35.0, (score / 100.0).coerceIn(0.0, 1.0))
+ }
+
+ private fun tokenize(text: String): List {
+ val words = text.split(Regex("\\s+")).filter { it.isNotBlank() }
+ return words.mapIndexed { index, word ->
+ Token(word, index, classifyToken(word))
+ }
+ }
+
+ private fun classifyToken(word: String) = when {
+ word.matches(Regex("\\d+(?:\\.\\d+)?\\s*(?:TB|GB|MB|PB)", RegexOption.IGNORE_CASE)) -> TokenType.CAPACITY
+ word.matches(Regex("\\d+")) -> TokenType.NUMBER
+ word.matches(Regex("[A-Z][a-z]+")) -> TokenType.PROPER_NOUN
+ word.matches(Regex("[A-Z]{2,}")) -> TokenType.ACRONYM
+ word.matches(Regex("[A-Z0-9\\-]+")) -> TokenType.MODEL_CODE
+ word.lowercase() in listOf("usb", "ssd", "hdd", "drive", "flash", "storage", "card") -> TokenType.PRODUCT_TYPE
+ else -> TokenType.WORD
+ }
+
+ private fun classifyLineType(line: String): LineType = when {
+ line.contains(Regex("\\d+\\s*(?:GB|TB|MB)", RegexOption.IGNORE_CASE)) -> LineType.CAPACITY_INFO
+ line.matches(Regex(".*(?:Model|SKU|Part|P/N|MPN).*", RegexOption.IGNORE_CASE)) -> LineType.MODEL_INFO
+ line.split("\\s+".toRegex()).size <= 4 && line.any { it.isUpperCase() } -> LineType.BRAND_OR_SERIES
+ line.contains("$") || line.contains(Regex("\\d+\\.\\d{2}")) -> LineType.PRICE_INFO
+ line.contains(Regex("\\d+\\s*x\\s*\\d+", RegexOption.IGNORE_CASE)) -> LineType.DIMENSIONS
+ else -> LineType.DESCRIPTIVE
+ }
+
+ private fun groupRelatedUnits(units: List): List {
+ val grouped = mutableListOf()
+ var i = 0
+
+ while (i < units.size) {
+ val current = units[i]
+ if (i + 1 < units.size) {
+ val next = units[i + 1]
+ if (shouldMerge(current, next)) {
+ grouped.add(
+ current.copy(
+ text = "${current.text} ${next.text}",
+ tokens = current.tokens + next.tokens
+ )
+ )
+ i += 2
+ continue
+ }
+ }
+ grouped.add(current)
+ i++
+ }
+ return grouped
+ }
+
+ private fun shouldMerge(a: SemanticUnit, b: SemanticUnit): Boolean {
+ if (abs(a.position - b.position) > 1) return false
+ if (a.type == LineType.BRAND_OR_SERIES && b.type == LineType.MODEL_INFO) return true
+ if (a.type == LineType.BRAND_OR_SERIES && b.type == LineType.CAPACITY_INFO) return true
+ if (a.text.length < 20 && b.text.length < 20 &&
+ a.type == LineType.DESCRIPTIVE && b.type == LineType.DESCRIPTIVE
+ ) return true
+ return false
+ }
+
+ private fun extractProductInformation(context: AnalysisContext): ProductInfo {
+ val info = ProductInfo()
+
+ val goodLines = context.lines.filter { it.quality.score > 45 }
+
+ if (context.rawText.length < 10 || goodLines.isEmpty()) {
+ info.name = "Unknown Item"
+ info.description = "Not enough readable text"
+ return info
+ }
+
+ // Extract in priority order
+ info.category = productTypeClassifier.classify(context)
+ info.brand = brandDatabase.findBrand(context, info.category, context.detectedLogos)
+ info.capacity = contextualExtractor.extractCapacity(context)
+ info.productLine = contextualExtractor.extractProductLine(context, info.brand)
+ info.modelNumber = contextualExtractor.extractModelNumber(context, info.brand, info.capacity, info.productLine)
+ info.name = nameBuilder.buildName(info, context)
+ info.description = generateSmartDescription(info, context)
+ info.condition = contextualExtractor.extractCondition(context)
+ info.price = contextualExtractor.extractPrice(context)
+ info.dimensions = contextualExtractor.extractDimensions(context)
+
+ return info
+ }
+
+ private fun generateSmartDescription(info: ProductInfo, context: AnalysisContext): String {
+ val parts = mutableListOf()
+
+ info.category?.let { parts.add(it) }
+ info.capacity?.let { parts.add("Capacity: $it") }
+
+ context.lines
+ .filter { it.quality.score > 55 }
+ .filter { it.type == LineType.DESCRIPTIVE }
+ .filter { line ->
+ !line.text.contains(info.brand ?: "", ignoreCase = true) &&
+ !line.text.contains(info.productLine ?: "", ignoreCase = true)
+ }
+ .take(2)
+ .forEach { parts.add(it.text) }
+
+ return parts.joinToString(" • ").ifBlank { "No description available" }
+ }
+
+ private fun calculateOverallConfidence(info: ProductInfo, context: AnalysisContext): Double {
+ var confidence = 0.0
+
+ if (info.brand != null) confidence += 30.0
+ if (info.productLine != null) confidence += 20.0
+ if (info.modelNumber != null) confidence += 15.0
+ if (info.capacity != null) confidence += 20.0
+ if (info.category != null) confidence += 10.0
+
+ val avgQuality = context.lines.mapNotNull { it.quality.score }.average()
+ if (!avgQuality.isNaN()) {
+ confidence += (avgQuality * 0.05).coerceAtMost(5.0)
+ }
+
+ return (confidence / 100.0).coerceIn(0.0, 1.0)
+ }
+
+ private fun cleanupTempFiles() {
+ try {
+ context.cacheDir.listFiles { it.name.startsWith("temp_") && it.name.endsWith(".jpg") }
+ ?.forEach { it.delete() }
+ } catch (_: Exception) {
+ // Ignore
+ }
+ }
+
+ private fun saveBitmapToTempUri(bitmap: Bitmap): Uri {
+ val file = java.io.File(context.cacheDir, "temp_${System.currentTimeMillis()}.jpg")
+ file.outputStream().use { bitmap.compress(Bitmap.CompressFormat.JPEG, 85, it) }
+ return Uri.fromFile(file)
+ }
+
+ // === INNER CLASSES ===
+
+ inner class LanguageDetector {
+ fun scoreEnglishLikelihood(text: String): Double {
+ var score = 0.5
+ val lowerText = text.lowercase()
+ val words = text.split(Regex("\\s+")).filter { it.length > 2 }
+
+ // English words
+ val englishCount = words.count { word ->
+ ENGLISH_TECH_WORDS.any { word.lowercase().contains(it) }
+ }
+ score += englishCount * 0.20
+
+ // Non-English words
+ val nonEnglishCount = words.count { word ->
+ NON_ENGLISH_INDICATORS.any { word.lowercase().contains(it) }
+ }
+ score -= nonEnglishCount * 0.30
+
+ // Accents
+ val accentedChars = text.count { it in "áéíóúñüàèìòùâêîôûëïöç" }
+ val accentRatio = if (text.length > 0) accentedChars.toDouble() / text.length else 0.0
+ score -= accentRatio * 3.0
+
+ // English patterns
+ if (lowerText.matches(Regex(".*\\b(the|and|with|for|from|this|that)\\b.*"))) {
+ score += 0.25
+ }
+
+ // Spanish/French patterns
+ if (lowerText.matches(Regex(".*\\b(de|el|la|le|du|des|une?|los|las)\\b.*"))) {
+ score -= 0.35
+ }
+
+ // Brands
+ val allBrands = brandDatabase.getAllBrands()
+ if (allBrands.any { lowerText.contains(it.lowercase()) }) {
+ score += 0.25
+ }
+
+ return score.coerceIn(0.0, 1.0)
+ }
+ }
+
+ inner class LogoDetector {
+ private val logoPatterns = mapOf(
+ "SanDisk" to listOf("sandisk", "san disk", "SANDISK"),
+ "Samsung" to listOf("samsung", "SAMSUNG"),
+ "Western Digital" to listOf("western digital", "wd", "WD", "westerndigital"),
+ "Kingston" to listOf("kingston", "KINGSTON"),
+ "Seagate" to listOf("seagate", "SEAGATE"),
+ "Crucial" to listOf("crucial", "CRUCIAL"),
+ "Corsair" to listOf("corsair", "CORSAIR"),
+ "PNY" to listOf("pny", "PNY"),
+ "Lexar" to listOf("lexar", "LEXAR"),
+ "Transcend" to listOf("transcend", "TRANSCEND")
+ )
+
+ fun detectLogos(text: String, labels: List): List {
+ val detected = mutableSetOf()
+ val lowerText = text.lowercase()
+
+ // Text-based detection
+ for ((brand, patterns) in logoPatterns) {
+ if (patterns.any { lowerText.contains(it.lowercase()) }) {
+ detected.add(brand)
+ Log.d(TAG, "Logo detected in text: $brand")
+ }
+ }
+
+ // Image label detection
+ for (label in labels) {
+ for ((brand, patterns) in logoPatterns) {
+ if (patterns.any { label.label.contains(it, ignoreCase = true) }) {
+ detected.add(brand)
+ Log.d(TAG, "Logo detected in image: $brand")
+ }
+ }
+ }
+
+ return detected.toList()
+ }
+ }
+
+ inner class IntelligentNameBuilder {
+ fun buildName(info: ProductInfo, context: AnalysisContext): String {
+ val parts = mutableListOf()
+
+ // Priority 1: Brand (from logo or text)
+ info.brand?.let { brand ->
+ parts.add(brand)
+ Log.d(TAG, "Name part 1: Brand = $brand")
+ }
+
+ // Priority 2: Product Line (Cruzer, Ultra, etc.)
+ info.productLine?.let { line ->
+ if (!parts.any { it.equals(line, ignoreCase = true) }) {
+ parts.add(line)
+ Log.d(TAG, "Name part 2: Product Line = $line")
+ }
+ }
+
+ // Priority 3: Capacity
+ info.capacity?.let { capacity ->
+ parts.add(capacity)
+ Log.d(TAG, "Name part 3: Capacity = $capacity")
+ }
+
+ // Priority 4: Category (if we don't have enough info)
+ if (parts.size < 2) {
+ info.category?.let { category ->
+ if (!parts.any { it.equals(category, ignoreCase = true) }) {
+ parts.add(category)
+ Log.d(TAG, "Name part 4: Category = $category")
+ }
+ }
+ }
+
+ // Fallback: use highest quality line
+ if (parts.isEmpty()) {
+ context.lines
+ .filter { it.quality.score > 60 }
+ .maxByOrNull { it.quality.score }
+ ?.let {
+ val cleaned = cleanNameText(it.text)
+ if (cleaned.isNotBlank()) {
+ parts.add(cleaned)
+ Log.d(TAG, "Name fallback: $cleaned")
+ }
+ }
+ }
+
+ val finalName = parts.joinToString(" ").ifBlank { "Unknown Item" }
+ Log.d(TAG, "=== Final Name: $finalName ===")
+ return finalName
+ }
+
+ private fun cleanNameText(text: String): String {
+ var cleaned = text
+
+ // Remove noise words
+ val words = cleaned.split(Regex("\\s+"))
+ val filtered = words.filter { word ->
+ !NOISE_WORDS.contains(word.lowercase()) &&
+ word.length > 1
+ }
+
+ cleaned = filtered.joinToString(" ").take(60)
+ return cleaned.trim()
+ }
+ }
+
+ inner class BrandDatabase {
+ private val brands = mapOf(
+ "storage" to listOf(
+ "SanDisk", "Samsung", "Western Digital", "WD", "Seagate",
+ "Kingston", "Crucial", "Intel", "Corsair", "PNY", "Transcend",
+ "Lexar", "ADATA", "G.SKILL", "TeamGroup", "Patriot", "Mushkin",
+ "Silicon Power", "Toshiba", "Verbatim", "Sony"
+ ),
+ "general" to listOf(
+ "Apple", "Sony", "LG", "HP", "Dell", "Lenovo", "ASUS", "Acer",
+ "Microsoft", "Google", "Amazon", "Logitech", "Razer", "Anker"
+ )
+ )
+
+ fun getAllBrands(): List = brands.values.flatten()
+
+ fun findBrand(context: AnalysisContext, category: String?, detectedLogos: List): String? {
+ // Priority 1: Detected logos
+ if (detectedLogos.isNotEmpty()) {
+ Log.d(TAG, "Brand from logo: ${detectedLogos.first()}")
+ return detectedLogos.first()
+ }
+
+ val allBrands = getAllBrands()
+ val categoryBrands = category?.let { brands[it.lowercase()] } ?: emptyList()
+
+ // Priority 2: Category-specific brands in high-quality lines
+ for (unit in context.lines.sortedByDescending { it.quality.score }.take(7)) {
+ for (brand in categoryBrands) {
+ if (unit.text.contains(brand, ignoreCase = true)) {
+ Log.d(TAG, "Brand from category search: $brand")
+ return brand
+ }
+ }
+ }
+
+ // Priority 3: Any brand in high-quality lines
+ for (unit in context.lines.sortedByDescending { it.quality.score }.take(7)) {
+ for (brand in allBrands) {
+ if (unit.text.contains(brand, ignoreCase = true)) {
+ Log.d(TAG, "Brand from general search: $brand")
+ return brand
+ }
+ }
+ }
+
+ // Priority 4: Image labels
+ for (label in context.imageLabels.filter { it.confidence > 0.6f }) {
+ for (brand in allBrands) {
+ if (label.label.contains(brand, ignoreCase = true)) {
+ Log.d(TAG, "Brand from image label: $brand")
+ return brand
+ }
+ }
+ }
+
+ return null
+ }
+ }
+
+ inner class ProductTypeClassifier {
+ private val typeKeywords = mapOf(
+ "USB Flash Drive" to listOf("usb", "flash", "drive", "stick", "pendrive", "thumb", "cruzer", "glide", "blade"),
+ "SSD" to listOf("ssd", "solid state", "nvme", "sata"),
+ "Hard Drive" to listOf("hdd", "hard drive", "disk drive", "mechanical"),
+ "Memory Card" to listOf("sd card", "microsd", "memory card", "flash card", "tf card"),
+ "External Storage" to listOf("external", "portable storage", "portable drive"),
+ "Power Supply" to listOf("power supply", "psu", "switching", "adapter", "charger", "ac/dc")
+ )
+
+ fun classify(context: AnalysisContext): String? {
+ val scores = mutableMapOf()
+
+ for ((type, keywords) in typeKeywords) {
+ var score = 0.0
+
+ for (unit in context.lines) {
+ for (keyword in keywords) {
+ if (unit.text.contains(keyword, ignoreCase = true)) {
+ score += (unit.quality.score / 100.0) * 1.5
+ }
+ }
+ }
+
+ for (label in context.imageLabels) {
+ for (keyword in keywords) {
+ if (label.label.contains(keyword, ignoreCase = true)) {
+ score += label.confidence * 2.5
+ }
+ }
+ }
+
+ scores[type] = score
+ }
+
+ val best = scores.filter { it.value > 0.6 }.maxByOrNull { it.value }
+ Log.d(TAG, "Category scores: $scores")
+ Log.d(TAG, "Selected category: ${best?.key}")
+ return best?.key
+ }
+ }
+
+ inner class ContextualExtractor {
+
+ fun extractCapacity(context: AnalysisContext): String? {
+ val regex = Regex("(\\d+(?:\\.\\d+)?\\s*(?:TB|GB|MB|PB))\\b", RegexOption.IGNORE_CASE)
+ val candidates = mutableListOf>()
+
+ for (unit in context.lines) {
+ regex.findAll(unit.text).forEach {
+ val capacity = it.value.trim().uppercase()
+ candidates.add(capacity to unit.quality.score)
+ }
+ }
+
+ val best = candidates.maxByOrNull { it.second }?.first
+ Log.d(TAG, "Extracted capacity: $best from ${candidates.size} candidates")
+ return best
+ }
+
+ fun extractProductLine(context: AnalysisContext, brand: String?): String? {
+ // SanDisk product lines
+ val sandiskLines = listOf("Cruzer", "Ultra", "Extreme", "Ultra Fit", "Ultra Flair", "Glide",
+ "Blade", "Switch", "Pop", "Edge", "Force", "Dual Drive", "iXpand")
+
+ // Other brand lines
+ val genericLines = listOf("Ultra", "Pro", "Extreme", "Evo", "Plus", "Max", "Elite",
+ "Gaming", "Premium", "Performance", "Essential", "Value", "Blue", "Black", "Red")
+
+ val searchLines = if (brand?.equals("SanDisk", ignoreCase = true) == true) {
+ sandiskLines + genericLines
+ } else {
+ genericLines
+ }
+
+ // First: Look for exact product line matches in high-quality lines
+ for (unit in context.lines.sortedByDescending { it.quality.score }.take(8)) {
+ for (line in searchLines) {
+ // Check if the product line appears as a distinct word
+ val pattern = Regex("\\b${Regex.escape(line)}\\b", RegexOption.IGNORE_CASE)
+ if (pattern.containsMatchIn(unit.text)) {
+ Log.d(TAG, "Found product line: $line in '${unit.text}'")
+ return line
+ }
+ }
+ }
+
+ // Second: Check for multi-word product lines (like "Ultra Fit")
+ for (unit in context.lines.sortedByDescending { it.quality.score }.take(8)) {
+ val multiWordLines = searchLines.filter { it.contains(" ") }
+ for (line in multiWordLines) {
+ if (unit.text.contains(line, ignoreCase = true)) {
+ Log.d(TAG, "Found multi-word product line: $line")
+ return line
+ }
+ }
+ }
+
+ return null
+ }
+
+ fun extractModelNumber(context: AnalysisContext, brand: String?, capacity: String?, productLine: String?): String? {
+ val patterns = listOf(
+ Regex("(?:Model|SKU|Part|P/N|MPN)[:\\s]+([A-Z0-9\\-]{4,20})", RegexOption.IGNORE_CASE),
+ Regex("\\b([A-Z]{2,4}\\d{3,}[A-Z0-9\\-]*)\\b"),
+ Regex("\\b(\\d{3,}[A-Z]{2,}[A-Z0-9\\-]*)\\b"),
+ brand?.let { Regex("${Regex.escape(it)}[\\s-]?([A-Z0-9\\-]{4,15})\\b", RegexOption.IGNORE_CASE) }
+ ).filterNotNull()
+
+ for (unit in context.lines.sortedByDescending { it.quality.score }.take(10)) {
+ // Skip very long descriptive lines
+ if (unit.text.length > 35 && unit.type == LineType.DESCRIPTIVE) continue
+
+ for (pattern in patterns) {
+ pattern.findAll(unit.text).forEach { match ->
+ val model = match.groupValues.getOrNull(1)?.trim() ?: match.value.trim()
+
+ // Validation checks
+ if (model.length !in 4..25) return@forEach
+
+ // Don't confuse capacity with model number
+ if (capacity != null && model.contains(capacity, ignoreCase = true)) return@forEach
+
+ // Don't confuse product line with model
+ if (productLine != null && model.equals(productLine, ignoreCase = true)) return@forEach
+
+ if (model.isBlank()) return@forEach
+ if (!model.any { it.isLetterOrDigit() }) return@forEach
+
+ // Must have mix of letters and numbers
+ val letters = model.count { it.isLetter() }
+ val digits = model.count { it.isDigit() }
+ if (letters < 2 || digits < 1) return@forEach
+
+ Log.d(TAG, "Found model number: $model")
+ return model
+ }
+ }
+ }
+ return null
+ }
+
+ fun extractCondition(context: AnalysisContext): String? {
+ val conditions = mapOf(
+ "New" to listOf("brand new", "new in box", "sealed", "unopened", "nib"),
+ "Like New" to listOf("like new", "mint", "excellent", "pristine", "barely used"),
+ "Good" to listOf("good", "used", "working", "functional"),
+ "Fair" to listOf("fair", "worn", "some wear"),
+ "Poor" to listOf("poor", "damaged", "broken", "for parts", "not working")
+ )
+
+ val text = context.rawText.lowercase()
+ for ((condition, keywords) in conditions) {
+ if (keywords.any { text.contains(it) }) {
+ Log.d(TAG, "Found condition: $condition")
+ return condition
+ }
+ }
+ return null
+ }
+
+ fun extractPrice(context: AnalysisContext): Double? {
+ val priceRegex = Regex("\\$\\s*([0-9,]+(?:\\.\\d{2})?)")
+ val match = priceRegex.find(context.rawText)
+ val priceString = match?.groupValues?.get(1)?.replace(",", "")
+ val price = priceString?.toDoubleOrNull()
+ return if (price != null && price in 0.01..999999.99) {
+ Log.d(TAG, "Found price: $price")
+ price
+ } else null
+ }
+
+ fun extractDimensions(context: AnalysisContext): String? {
+ val dimensionsRegex = Regex(
+ "(\\d+(?:\\.\\d+)?)\\s*[xX×]\\s*(\\d+(?:\\.\\d+)?)(?:\\s*[xX×]\\s*(\\d+(?:\\.\\d+)?))?\\s*(in|inch|cm|mm)?",
+ RegexOption.IGNORE_CASE
+ )
+ val result = dimensionsRegex.find(context.rawText)?.value?.trim()
+ if (result != null) {
+ Log.d(TAG, "Found dimensions: $result")
+ }
+ return result
+ }
+ }
+
+ // Data classes
+ data class AIAnalysisResult(
+ val itemName: String?,
+ val modelNumber: String?,
+ val description: String?,
+ val condition: String?,
+ val sizeCategory: String?,
+ val estimatedPrice: Double?,
+ val dimensions: String?,
+ val rawText: String?,
+ val confidence: Double
+ )
+
+ data class AnalysisContext(
+ val rawText: String,
+ val imageLabels: List,
+ val lines: List,
+ val detectedLogos: List = emptyList()
+ )
+
+ data class LabelInfo(val label: String, val confidence: Float)
+
+ data class SemanticUnit(
+ val text: String,
+ val originalText: String,
+ val position: Int,
+ val quality: LineQuality,
+ val tokens: List,
+ val type: LineType
+ )
+
+ data class LineQuality(val score: Double, val isUseful: Boolean, val confidence: Double)
+
+ data class Token(val text: String, val position: Int, val type: TokenType)
+
+ enum class TokenType { CAPACITY, NUMBER, PROPER_NOUN, ACRONYM, MODEL_CODE, PRODUCT_TYPE, WORD }
+
+ enum class LineType { BRAND_OR_SERIES, CAPACITY_INFO, MODEL_INFO, PRICE_INFO, DIMENSIONS, DESCRIPTIVE }
+
+ data class ProductInfo(
+ var name: String = "",
+ var brand: String? = null,
+ var modelNumber: String? = null,
+ var productLine: String? = null,
+ var capacity: String? = null,
+ var category: String? = null,
+ var description: String = "",
+ var condition: String? = null,
+ var price: Double? = null,
+ var dimensions: String? = null
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/services/AIServicess.kt b/app/src/main/java/com/samuel/inventorymanager/services/AIServicess.kt
new file mode 100644
index 0000000..7368cec
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/services/AIServicess.kt
@@ -0,0 +1,177 @@
+//package com.samuel.inventorymanager.services
+//
+//import android.content.Context
+//import android.graphics.Bitmap
+//import android.util.Base64
+//import android.util.Log
+//import com.samuel.inventorymanager.data.AISettings
+//import com.samuel.inventorymanager.screens.AIAnalysisResult
+//import kotlinx.coroutines.Dispatchers
+//import kotlinx.coroutines.withContext
+//import org.json.JSONObject
+//import java.io.ByteArrayOutputStream
+//import java.net.HttpURLConnection
+//import java.net.URL
+//
+//class AIService(private val context: Context) {
+//
+// suspend fun analyzeItemFromBitmap(
+// bitmap: Bitmap,
+// aiSettings: AISettings = AISettings()
+// ): AIAnalysisResult = withContext(Dispatchers.IO) {
+// try {
+// // Check if API key exists
+// if (aiSettings.anthropicApiKey.isNullOrBlank()) {
+// throw Exception("AI API Key is missing. Please configure it in Settings.")
+// }
+//
+// // 1. Convert bitmap to base64
+// val base64Image = bitmapToBase64(bitmap)
+//
+// // 2. Call Claude API
+// val response = callClaudeAPI(base64Image, aiSettings)
+//
+// // 3. Parse response
+// parseAIResponse(response)
+// } catch (e: Exception) {
+// Log.e("AIService", "Analysis failed", e)
+// // Return an empty result with the error in description instead of crashing
+// AIAnalysisResult(
+// description = "AI Analysis failed: ${e.message}",
+// rawText = "Error during processing."
+// )
+// }
+// }
+//
+// private suspend fun callClaudeAPI(
+// base64Image: String?,
+// aiSettings: AISettings,
+// customPrompt: String? = null
+// ): String = withContext(Dispatchers.IO) {
+// val url = URL("https://api.anthropic.com/v1/messages")
+// val connection = url.openConnection() as HttpURLConnection
+//
+// try {
+// connection.requestMethod = "POST"
+// connection.setRequestProperty("Content-Type", "application/json")
+// connection.setRequestProperty("x-api-key", aiSettings.anthropicApiKey)
+// connection.setRequestProperty("anthropic-version", "2023-06-01")
+// connection.doOutput = true
+//
+// val prompt = customPrompt ?: buildAnalysisPrompt()
+// val requestBody = buildJSONRequest(prompt, base64Image)
+//
+// connection.outputStream.use { it.write(requestBody.toByteArray()) }
+//
+// val responseCode = connection.responseCode
+// if (responseCode == 200) {
+// connection.inputStream.bufferedReader().use { it.readText() }
+// } else {
+// val error = connection.errorStream?.bufferedReader()?.use { it.readText() }
+// throw Exception("API Error ($responseCode): $error")
+// }
+// } finally {
+// connection.disconnect()
+// }
+// }
+//
+// private fun buildAnalysisPrompt(): String = """
+// Analyze this item image and extract the following information in JSON format:
+// {
+// "itemName": "The main product name",
+// "modelNumber": "Model/SKU if visible",
+// "description": "Brief description (2-3 sentences)",
+// "condition": "New/Like New/Good/Fair/Poor based on appearance",
+// "sizeCategory": "Small/Medium/Large/Extra Large",
+// "estimatedPrice": numeric value in USD (just the number),
+// "dimensions": "Estimated dimensions (e.g. 10x10x5 inches) if visual cues exist",
+// "detectedText": "Any text visible in the image"
+// }
+// Be specific. If information is not clearly visible, use null.
+// Respond ONLY with valid JSON.
+// """.trimIndent()
+//
+// private fun buildJSONRequest(
+// prompt: String,
+// base64Image: String?
+// ): String {
+// val contentParts = mutableListOf()
+//
+// if (base64Image != null) {
+// // Image block
+// contentParts.add("""
+// {
+// "type": "image",
+// "source": {
+// "type": "base64",
+// "media_type": "image/jpeg",
+// "data": "$base64Image"
+// }
+// }
+// """.trimIndent())
+// }
+//
+// // Text block (Prompt)
+// contentParts.add("""
+// {
+// "type": "text",
+// "text": "$prompt"
+// }
+// """.trimIndent())
+//
+// // Construct final JSON safely
+// return """
+// {
+// "model": "claude-3-5-sonnet-20241022",
+// "max_tokens": 1024,
+// "messages": [
+// {
+// "role": "user",
+// "content": [${contentParts.joinToString(",")}]
+// }
+// ]
+// }
+// """.trimIndent()
+// }
+//
+// private fun parseAIResponse(jsonResponse: String): AIAnalysisResult {
+// try {
+// val rootJson = JSONObject(jsonResponse)
+// val contentArray = rootJson.getJSONArray("content")
+// val textContent = contentArray.getJSONObject(0).getString("text")
+//
+// // Clean markdown code blocks if Claude adds them
+// val cleanJsonString = textContent
+// .replace("```json", "")
+// .replace("```", "")
+// .trim()
+//
+// val resultJson = JSONObject(cleanJsonString)
+//
+// return AIAnalysisResult(
+// itemName = resultJson.optString("itemName").takeIf { it.isNotEmpty() },
+// modelNumber = resultJson.optString("modelNumber").takeIf { it.isNotEmpty() },
+// description = resultJson.optString("description").takeIf { it.isNotEmpty() },
+// condition = resultJson.optString("condition").takeIf { it.isNotEmpty() },
+// sizeCategory = resultJson.optString("sizeCategory").takeIf { it.isNotEmpty() },
+// estimatedPrice = resultJson.optDouble("estimatedPrice").takeIf { !it.isNaN() },
+// dimensions = resultJson.optString("dimensions").takeIf { it.isNotEmpty() },
+// // Map AI "detectedText" to our Data Model "rawText"
+// rawText = resultJson.optString("detectedText").takeIf { it.isNotEmpty() }
+// )
+// } catch (e: Exception) {
+// Log.e("AIService", "JSON Parsing failed", e)
+// return AIAnalysisResult(
+// description = "Could not parse AI response. Raw response: $jsonResponse"
+// )
+// }
+// }
+//
+// private fun bitmapToBase64(bitmap: Bitmap): String {
+// val outputStream = ByteArrayOutputStream()
+// // Compress to 70% quality to save bandwidth/tokens
+// bitmap.compress(Bitmap.CompressFormat.JPEG, 70, outputStream)
+// val bytes = outputStream.toByteArray()
+// return Base64.encodeToString(bytes, Base64.NO_WRAP)
+// }
+//}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/services/OCRService.kt b/app/src/main/java/com/samuel/inventorymanager/services/OCRService.kt
new file mode 100644
index 0000000..334a9bc
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/services/OCRService.kt
@@ -0,0 +1,92 @@
+package com.samuel.inventorymanager.services
+
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import com.google.mlkit.vision.common.InputImage
+import com.google.mlkit.vision.text.TextRecognition
+import com.google.mlkit.vision.text.latin.TextRecognizerOptions
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.tasks.await
+import kotlinx.coroutines.withContext
+import java.io.IOException
+import kotlin.math.roundToInt
+
+// Simplified data model
+data class OCRResult(
+ val text: String,
+ val confidence: Double = 0.0,
+ val provider: String = "ML Kit (Free)"
+)
+
+class OCRService(context: Context) {
+
+ // Use Application Context to prevent Activity leaks
+ private val applicationContext = context.applicationContext
+ private val TAG = "OCRService"
+
+ // Lazy initialization: Client is only created once when needed, saving memory.
+ private val recognizer by lazy {
+ TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
+ }
+
+ suspend fun performOCR(imageUri: Uri): OCRResult = withContext(Dispatchers.IO) {
+ try {
+ Log.d(TAG, "Starting OCR for Image: $imageUri")
+
+ // 1. Pre-check: Ensure image loadable
+ val image: InputImage
+ try {
+ image = InputImage.fromFilePath(applicationContext, imageUri)
+ } catch (e: IOException) {
+ Log.e(TAG, "Failed to load image from path", e)
+ return@withContext OCRResult("", 0.0, "Error: Image Not Found")
+ }
+
+ // 2. Process the image
+ val result = recognizer.process(image).await()
+ val rawText = result.text
+
+ // 3. Smart Confidence Calculation
+ // TextBlock and Line do not have confidence in ML Kit V2, only 'Elements' (words) do.
+ val allConfidences = result.textBlocks
+ .flatMap { it.lines }
+ .flatMap { it.elements }
+ .mapNotNull { it.confidence }
+
+ // If we found words, calculate average. If not, 0.0
+ val finalConfidence = if (allConfidences.isNotEmpty()) {
+ allConfidences.average()
+ } else {
+ // If text was found but no confidence data exists (rare), default to generic high or low
+ if (rawText.isNotEmpty()) 0.80 else 0.0
+ }
+
+ // 4. Round confidence for cleaner UI (e.g. 0.92 instead of 0.9234512)
+ val roundedConfidence = (finalConfidence * 100.0).roundToInt() / 100.0
+
+ if (rawText.isBlank()) {
+ Log.w(TAG, "OCR finished but found no text.")
+ } else {
+ Log.i(TAG, "OCR Success. Confidence: $roundedConfidence")
+ }
+
+ OCRResult(
+ text = rawText.trim(), // Smart clean: Remove extra whitespace
+ confidence = roundedConfidence,
+ provider = "ML Kit (On-Device)"
+ )
+
+ } catch (e: Exception) {
+ Log.e(TAG, "OCR Critical Failure", e)
+ // Return an empty result or rethrow depending on your app's needs.
+ // Rethrowing with a clear message allows the ViewModel to handle the UI state "Error"
+ throw Exception("Failed to scan text: ${e.localizedMessage}")
+ }
+ }
+
+ // Backward compatibility: Keeps existing code working while ignoring unused settings
+ suspend fun performOCR(imageUri: Uri, ocrSettings: Any?): OCRResult {
+ return performOCR(imageUri)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/services/Services.kt b/app/src/main/java/com/samuel/inventorymanager/services/Services.kt
new file mode 100644
index 0000000..449c533
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/services/Services.kt
@@ -0,0 +1,49 @@
+//package com.samuel.inventorymanager.services
+//
+//import android.content.Context
+//import android.net.Uri
+//import com.samuel.inventorymanager.screens.AIAnalysisResult
+//import kotlinx.coroutines.delay
+//
+//// --- FAKE AI SERVICE (REPLACE WITH YOUR REAL IMPLEMENTATION) ---
+//class AIService(private val context: Context, private val apiKey: String) {
+//
+// // This is a placeholder. Replace with your actual AI API call.
+// suspend fun analyzeItem(prompt: String, imageUri: Uri?): AIAnalysisResult {
+// // Simulate network delay
+// delay(3000)
+//
+// // Simulate a successful AI response
+// // In a real app, you would make a network request to an AI service
+// // and parse the JSON response into the AIAnalysisResult object.
+// return AIAnalysisResult(
+// itemName = "Vintage Sony Walkman",
+// modelNumber = "WM-F45",
+// description = "A portable cassette player from the late 1980s. Features AM/FM radio, auto-reverse, and Mega Bass. Requires two AA batteries. Shows minor signs of wear but is fully functional.",
+// condition = "Good",
+// estimatedPrice = 75.0
+// )
+// }
+//}
+//
+//// --- FAKE OCR SERVICE (REPLACE WITH YOUR REAL IMPLEMENTATION) ---
+//class OCRService(private val context: Context) {
+//
+// data class OCRResult(val text: String, val provider: String)
+//
+// // This is a placeholder. Replace with a real OCR library (e.g., Google ML Kit).
+// suspend fun performOCR(imageUri: Uri): OCRResult {
+// // Simulate processing delay
+// delay(2000)
+//
+// // Simulate a successful OCR response
+// return OCRResult(
+// text = """
+// Sony Corporation
+// WM-F45
+// Made in Japan
+// """.trimIndent(),
+// provider = "Simulated OCR"
+// )
+// }
+//}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/ui/theme/Theme.kt b/app/src/main/java/com/samuel/inventorymanager/ui/theme/Theme.kt
index fa781d8..645d00b 100644
--- a/app/src/main/java/com/samuel/inventorymanager/ui/theme/Theme.kt
+++ b/app/src/main/java/com/samuel/inventorymanager/ui/theme/Theme.kt
@@ -1,16 +1,12 @@
package com.samuel.inventorymanager.ui.theme
-import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography
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.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
@@ -20,18 +16,19 @@ import androidx.compose.ui.unit.sp
// COLOR DEFINITIONS
// ========================================================================================
+// Base Theme Colors
private val PurpleLight40 = Color(0xFF6650a4)
private val PurpleGreyLight40 = Color(0xFF625b71)
private val PinkLight40 = Color(0xFF7D5260)
-private val PurpleDark80 = Color(0xFFEADDF5)
-private val PurpleGreyDark80 = Color(0xFFCCC7DB)
+private val PurpleDark80 = Color(0xFFD0BCFF)
+private val PurpleGreyDark80 = Color(0xFFCCC2DC)
private val PinkDark80 = Color(0xFFEFB8C8)
-// Additional Theme Colors
private val LightBackground = Color(0xFFFFFBFE)
private val DarkBackground = Color(0xFF1A1A1A)
+// Custom Theme Primary Colors
private val DraculaPrimary = Color(0xFFBD93F9)
private val VampirePrimary = Color(0xFFFF1493)
private val OceanPrimary = Color(0xFF00B4D8)
@@ -40,6 +37,19 @@ private val SunsetPrimary = Color(0xFFFF6B35)
private val CyberpunkPrimary = Color(0xFFFF006E)
private val NeonPrimary = Color(0xFF39FF14)
+// New Indigo Theme Colors (from second file)
+private val IndigoPrimaryLight = Color(0xFF6366F1)
+private val EmeraldSecondaryLight = Color(0xFF10B981)
+private val RedErrorLight = Color(0xFFEF4444)
+private val SlateBackgroundLight = Color(0xFFF8FAFC)
+private val WhiteSurfaceLight = Color(0xFFFEFEFE)
+
+private val IndigoPrimaryDark = Color(0xFF818CF8) // Lighter indigo for dark mode
+private val EmeraldSecondaryDark = Color(0xFF34D399) // Lighter emerald for dark mode
+private val RedErrorDark = Color(0xFFF87171) // Lighter red for dark mode
+private val SlateBackgroundDark = Color(0xFF0F172A) // Dark slate for background
+private val SlateSurfaceDark = Color(0xFF1E293B) // Slightly lighter slate for surface
+
// ========================================================================================
// COLOR SCHEMES
// ========================================================================================
@@ -70,6 +80,24 @@ private val DarkColorScheme = darkColorScheme(
onSurface = Color.White
)
+// New Indigo Theme Scheme
+private val IndigoLightScheme = lightColorScheme(
+ primary = IndigoPrimaryLight,
+ secondary = EmeraldSecondaryLight,
+ error = RedErrorLight,
+ background = SlateBackgroundLight,
+ surface = WhiteSurfaceLight
+)
+
+private val IndigoDarkScheme = darkColorScheme(
+ primary = IndigoPrimaryDark,
+ secondary = EmeraldSecondaryDark,
+ error = RedErrorDark,
+ background = SlateBackgroundDark,
+ surface = SlateSurfaceDark
+)
+
+
private val DraculaLightScheme = lightColorScheme(
primary = DraculaPrimary,
secondary = Color(0xFF8BE9FD),
@@ -272,7 +300,7 @@ fun getScaledTypography(scale: Float): Typography {
// ========================================================================================
enum class AppThemeType {
- LIGHT, DARK, DRACULA, VAMPIRE, OCEAN, FOREST, SUNSET, CYBERPUNK, NEON
+ LIGHT, DARK, INDIGO, DRACULA, VAMPIRE, OCEAN, FOREST, SUNSET, CYBERPUNK, NEON
}
// ========================================================================================
@@ -284,25 +312,19 @@ fun InventoryManagerTheme(
themeType: AppThemeType = AppThemeType.LIGHT,
darkTheme: Boolean = isSystemInDarkTheme(),
fontScale: Float = 1.0f,
- dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
- val colorScheme = when {
- themeType == AppThemeType.LIGHT -> LightColorScheme
- themeType == AppThemeType.DARK -> DarkColorScheme
- themeType == AppThemeType.DRACULA -> if (darkTheme) DraculaDarkScheme else DraculaLightScheme
- themeType == AppThemeType.VAMPIRE -> if (darkTheme) VampireDarkScheme else VampireLightScheme
- themeType == AppThemeType.OCEAN -> if (darkTheme) OceanDarkScheme else OceanLightScheme
- themeType == AppThemeType.FOREST -> if (darkTheme) ForestDarkScheme else ForestLightScheme
- themeType == AppThemeType.SUNSET -> if (darkTheme) SunsetDarkScheme else SunsetLightScheme
- themeType == AppThemeType.CYBERPUNK -> if (darkTheme) CyberpunkDarkScheme else CyberpunkLightScheme
- themeType == AppThemeType.NEON -> if (darkTheme) NeonDarkScheme else NeonLightScheme
- 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 colorScheme = when (themeType) {
+ AppThemeType.LIGHT -> LightColorScheme
+ AppThemeType.DARK -> DarkColorScheme
+ AppThemeType.INDIGO -> if (darkTheme) IndigoDarkScheme else IndigoLightScheme
+ AppThemeType.DRACULA -> if (darkTheme) DraculaDarkScheme else DraculaLightScheme
+ AppThemeType.VAMPIRE -> if (darkTheme) VampireDarkScheme else VampireLightScheme
+ AppThemeType.OCEAN -> if (darkTheme) OceanDarkScheme else OceanLightScheme
+ AppThemeType.FOREST -> if (darkTheme) ForestDarkScheme else ForestLightScheme
+ AppThemeType.SUNSET -> if (darkTheme) SunsetDarkScheme else SunsetLightScheme
+ AppThemeType.CYBERPUNK -> if (darkTheme) CyberpunkDarkScheme else CyberpunkLightScheme
+ AppThemeType.NEON -> if (darkTheme) NeonDarkScheme else NeonLightScheme
}
val scaledTypography = getScaledTypography(fontScale)
diff --git a/app/src/main/java/com/samuel/inventorymanager/viewmodels/AuthViewModel.kt b/app/src/main/java/com/samuel/inventorymanager/viewmodels/AuthViewModel.kt
new file mode 100644
index 0000000..5fc7c52
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/viewmodels/AuthViewModel.kt
@@ -0,0 +1,134 @@
+package com.samuel.inventorymanager.viewmodels
+
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.IntentSenderRequest
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.android.gms.auth.api.identity.BeginSignInRequest
+import com.google.android.gms.auth.api.identity.Identity
+import com.google.android.gms.auth.api.identity.SignInClient
+import com.google.firebase.auth.FirebaseAuth
+import com.google.firebase.auth.GoogleAuthProvider
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.tasks.await
+
+class AuthViewModel : ViewModel() {
+
+ private val _authState = MutableStateFlow(AuthState.Loading)
+ val authState: StateFlow = _authState
+
+ private val _isFirstLaunch = MutableStateFlow(true)
+ val isFirstLaunch: StateFlow = _isFirstLaunch
+
+ private lateinit var oneTapClient: SignInClient
+ private val auth = FirebaseAuth.getInstance()
+
+ fun initialize(context: Context) {
+ oneTapClient = Identity.getSignInClient(context)
+ checkAuthState(context)
+ }
+
+ private fun checkAuthState(context: Context) {
+ viewModelScope.launch {
+ try {
+ // Check if user has completed onboarding
+ val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
+ val hasCompletedOnboarding = prefs.getBoolean("completed_onboarding", false)
+ _isFirstLaunch.value = !hasCompletedOnboarding
+
+ // Check if user is signed in
+ val currentUser = auth.currentUser
+ if (currentUser != null && hasCompletedOnboarding) {
+ _authState.value = AuthState.Authenticated(currentUser.email ?: "User")
+ } else if (hasCompletedOnboarding) {
+ _authState.value = AuthState.NotAuthenticated
+ } else {
+ _authState.value = AuthState.ShowOnboarding
+ }
+ } catch (e: Exception) {
+ _authState.value = AuthState.Error(e.message ?: "Unknown error")
+ }
+ }
+ }
+
+ fun completeOnboarding(context: Context) {
+ val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
+ prefs.edit().putBoolean("completed_onboarding", true).apply()
+ _isFirstLaunch.value = false
+ _authState.value = AuthState.NotAuthenticated
+ }
+
+ fun startGoogleSignIn(
+ context: Context,
+ launcher: ActivityResultLauncher
+ ) {
+ viewModelScope.launch {
+ try {
+ _authState.value = AuthState.Loading
+
+ val signInRequest = BeginSignInRequest.builder()
+ .setGoogleIdTokenRequestOptions(
+ BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
+ .setSupported(true)
+ // Replace with your actual Web Client ID from Firebase Console
+ .setServerClientId("604064044455-7rb4vc1arekkbi59999aprp8hamrtkhq.apps.googleusercontent.com")
+ .setFilterByAuthorizedAccounts(false)
+ .build()
+ )
+ .build()
+
+ val result = oneTapClient.beginSignIn(signInRequest).await()
+ val intentSenderRequest = IntentSenderRequest.Builder(result.pendingIntent).build()
+ launcher.launch(intentSenderRequest)
+
+ } catch (e: Exception) {
+ _authState.value = AuthState.Error("Sign in failed: ${e.message}")
+ }
+ }
+ }
+
+ fun handleSignInResult(intent: Intent?) {
+ viewModelScope.launch {
+ try {
+ val credential = oneTapClient.getSignInCredentialFromIntent(intent)
+ val idToken = credential.googleIdToken
+
+ if (idToken != null) {
+ val firebaseCredential = GoogleAuthProvider.getCredential(idToken, null)
+ val authResult = auth.signInWithCredential(firebaseCredential).await()
+ val user = authResult.user
+
+ if (user != null) {
+ _authState.value = AuthState.Authenticated(user.email ?: "User")
+ } else {
+ _authState.value = AuthState.Error("Authentication failed")
+ }
+ } else {
+ _authState.value = AuthState.Error("No ID token received")
+ }
+ } catch (e: Exception) {
+ _authState.value = AuthState.Error("Sign in error: ${e.message}")
+ }
+ }
+ }
+
+ fun signOut(context: Context) {
+ viewModelScope.launch {
+ auth.signOut()
+ oneTapClient.signOut().await()
+ _authState.value = AuthState.NotAuthenticated
+ }
+ }
+}
+
+sealed class AuthState {
+ object Loading : AuthState()
+ object ShowOnboarding : AuthState()
+ object NotAuthenticated : AuthState()
+ data class Authenticated(val email: String) : AuthState()
+ data class Error(val message: String) : AuthState()
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/inventorymanger_background.xml b/app/src/main/res/drawable/inventorymanger_background.xml
new file mode 100644
index 0000000..ca3826a
--- /dev/null
+++ b/app/src/main/res/drawable/inventorymanger_background.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/inventorymanger.xml b/app/src/main/res/mipmap-anydpi-v26/inventorymanger.xml
new file mode 100644
index 0000000..874170e
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/inventorymanger.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/inventorymanger_round.xml b/app/src/main/res/mipmap-anydpi-v26/inventorymanger_round.xml
new file mode 100644
index 0000000..874170e
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/inventorymanger_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/inventorymanger.webp b/app/src/main/res/mipmap-hdpi/inventorymanger.webp
new file mode 100644
index 0000000..72259b5
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/inventorymanger.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/inventorymanger_foreground.webp b/app/src/main/res/mipmap-hdpi/inventorymanger_foreground.webp
new file mode 100644
index 0000000..c50c46b
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/inventorymanger_foreground.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/inventorymanger_round.webp b/app/src/main/res/mipmap-hdpi/inventorymanger_round.webp
new file mode 100644
index 0000000..505a778
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/inventorymanger_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/inventorymanger.webp b/app/src/main/res/mipmap-mdpi/inventorymanger.webp
new file mode 100644
index 0000000..a5bac1c
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/inventorymanger.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/inventorymanger_foreground.webp b/app/src/main/res/mipmap-mdpi/inventorymanger_foreground.webp
new file mode 100644
index 0000000..9bec189
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/inventorymanger_foreground.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/inventorymanger_round.webp b/app/src/main/res/mipmap-mdpi/inventorymanger_round.webp
new file mode 100644
index 0000000..07eda00
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/inventorymanger_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/inventorymanger.webp b/app/src/main/res/mipmap-xhdpi/inventorymanger.webp
new file mode 100644
index 0000000..cbbfbd8
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/inventorymanger.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/inventorymanger_foreground.webp b/app/src/main/res/mipmap-xhdpi/inventorymanger_foreground.webp
new file mode 100644
index 0000000..eaa08db
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/inventorymanger_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/inventorymanger_round.webp b/app/src/main/res/mipmap-xhdpi/inventorymanger_round.webp
new file mode 100644
index 0000000..9f2b785
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/inventorymanger_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/inventorymanger.webp b/app/src/main/res/mipmap-xxhdpi/inventorymanger.webp
new file mode 100644
index 0000000..d363c3a
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/inventorymanger.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/inventorymanger_foreground.webp b/app/src/main/res/mipmap-xxhdpi/inventorymanger_foreground.webp
new file mode 100644
index 0000000..ff05c79
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/inventorymanger_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/inventorymanger_round.webp b/app/src/main/res/mipmap-xxhdpi/inventorymanger_round.webp
new file mode 100644
index 0000000..aa6a30e
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/inventorymanger_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/inventorymanger.webp b/app/src/main/res/mipmap-xxxhdpi/inventorymanger.webp
new file mode 100644
index 0000000..56a3b25
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/inventorymanger.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/inventorymanger_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/inventorymanger_foreground.webp
new file mode 100644
index 0000000..356ec5b
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/inventorymanger_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/inventorymanger_round.webp b/app/src/main/res/mipmap-xxxhdpi/inventorymanger_round.webp
new file mode 100644
index 0000000..406ac75
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/inventorymanger_round.webp differ
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..4600e6c
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml
index 9ac162a..f5eb1ad 100644
--- a/app/src/main/res/xml/provider_paths.xml
+++ b/app/src/main/res/xml/provider_paths.xml
@@ -1,4 +1,22 @@
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index f7b5371..f0e74b0 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,4 +1,9 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1c3d1c4..0ec29da 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,13 +1,13 @@
[versions]
agp = "8.13.0"
-kotlin = "2.0.21"
+kotlin = "2.2.21"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
-lifecycleRuntimeKtx = "2.9.4"
-activityCompose = "1.11.0"
-composeBom = "2024.09.00"
+lifecycleRuntimeKtx = "2.10.0"
+activityCompose = "1.12.1"
+composeBom = "2025.12.00"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -28,5 +28,8 @@ androidx-compose-material3 = { group = "androidx.compose.material3", name = "mat
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+#noinspection SimilarGradleDependency
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+#noinspection SimilarGradleDependency
+compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version = "2.2.21" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index fc48852..fd65ced 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,24 +1,21 @@
+// In your C:\Garage\AndroidStudioCode\settings.gradle.kts file
+
pluginManagement {
repositories {
- google {
- content {
- includeGroupByRegex("com\\.android.*")
- includeGroupByRegex("com\\.google.*")
- includeGroupByRegex("androidx.*")
- }
- }
+ google()
mavenCentral()
gradlePluginPortal()
}
}
+
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
- mavenCentral()
+ mavenCentral() // THIS IS CRITICAL
+ maven("https://jitpack.io")
}
}
-rootProject.name = "InventoryManager"
-include(":app")
-
\ No newline at end of file
+rootProject.name = "Inventory Manager"
+include(":app")
\ No newline at end of file