From e900e868aed1ee4a83c83e2005c42c236c81c2ee Mon Sep 17 00:00:00 2001 From: JohnJackson12 Date: Sat, 6 Dec 2025 19:46:51 -0500 Subject: [PATCH] Adding a bunch of files for 2.0 AI, export,gmail and lots more. --- app/build.gradle.kts | 113 +- app/src/main/AndroidManifest.xml | 28 +- app/src/main/inventorymanger-playstore.png | Bin 0 -> 253113 bytes .../Activity/ImageEditorActivity.kt | 103 + .../samuel/inventorymanager/MainActivity.kt | 65 +- .../inventorymanager/data/AppSettings.kt | 36 +- .../screens/AIProcessingScreen.kt | 306 ++ .../screens/AIResultsScreen.kt | 415 +++ .../screens/CreateItemScreen.kt | 1374 +++++--- .../screens/CreateItemVewModel.kt | 220 +- .../inventorymanager/screens/DataModels.kt | 66 +- .../screens/GoogleSyncScreen.kt | 890 ++++++ .../inventorymanager/screens/HelpScreen.kt | 44 +- .../inventorymanager/screens/HistoryScreen.kt | 571 ++-- .../screens/ImageEditScreen.kt | 843 +++++ .../screens/ImageProcessingScreen.kt | 845 +++++ .../inventorymanager/screens/ImagesScreen.kt | 613 +++- .../screens/LocationsScreen.kt | 2840 ++++++++++++++++- .../inventorymanager/screens/MainAppScreen.kt | 1484 +++++++-- .../screens/OnboardingScreen.kt | 493 +++ .../screens/OverviewScreen.kt | 1159 ++++--- .../inventorymanager/screens/SearchScreen.kt | 303 +- .../inventorymanager/screens/SettingScreen.kt | 1266 ++------ .../inventorymanager/screens/ShareScreen.kt | 1047 ++++++ .../inventorymanager/services/AIService.kt | 890 ++++++ .../inventorymanager/services/AIServicess.kt | 177 + .../inventorymanager/services/OCRService.kt | 92 + .../inventorymanager/services/Services.kt | 49 + .../samuel/inventorymanager/ui/theme/Theme.kt | 72 +- .../viewmodels/AuthViewModel.kt | 134 + .../drawable/inventorymanger_background.xml | 74 + .../res/mipmap-anydpi-v26/inventorymanger.xml | 5 + .../inventorymanger_round.xml | 5 + .../main/res/mipmap-hdpi/inventorymanger.webp | Bin 0 -> 4090 bytes .../inventorymanger_foreground.webp | Bin 0 -> 8988 bytes .../mipmap-hdpi/inventorymanger_round.webp | Bin 0 -> 6072 bytes .../main/res/mipmap-mdpi/inventorymanger.webp | Bin 0 -> 2446 bytes .../inventorymanger_foreground.webp | Bin 0 -> 4852 bytes .../mipmap-mdpi/inventorymanger_round.webp | Bin 0 -> 3390 bytes .../res/mipmap-xhdpi/inventorymanger.webp | Bin 0 -> 6282 bytes .../inventorymanger_foreground.webp | Bin 0 -> 13650 bytes .../mipmap-xhdpi/inventorymanger_round.webp | Bin 0 -> 8820 bytes .../res/mipmap-xxhdpi/inventorymanger.webp | Bin 0 -> 10982 bytes .../inventorymanger_foreground.webp | Bin 0 -> 26874 bytes .../mipmap-xxhdpi/inventorymanger_round.webp | Bin 0 -> 15274 bytes .../res/mipmap-xxxhdpi/inventorymanger.webp | Bin 0 -> 17342 bytes .../inventorymanger_foreground.webp | Bin 0 -> 47078 bytes .../mipmap-xxxhdpi/inventorymanger_round.webp | Bin 0 -> 23398 bytes app/src/main/res/xml/file_paths.xml | 4 + app/src/main/res/xml/provider_paths.xml | 22 +- build.gradle.kts | 7 +- gradle/libs.versions.toml | 11 +- settings.gradle.kts | 19 +- 53 files changed, 13960 insertions(+), 2725 deletions(-) create mode 100644 app/src/main/inventorymanger-playstore.png create mode 100644 app/src/main/java/com/samuel/inventorymanager/Activity/ImageEditorActivity.kt create mode 100644 app/src/main/java/com/samuel/inventorymanager/screens/AIProcessingScreen.kt create mode 100644 app/src/main/java/com/samuel/inventorymanager/screens/AIResultsScreen.kt create mode 100644 app/src/main/java/com/samuel/inventorymanager/screens/GoogleSyncScreen.kt create mode 100644 app/src/main/java/com/samuel/inventorymanager/screens/ImageEditScreen.kt create mode 100644 app/src/main/java/com/samuel/inventorymanager/screens/ImageProcessingScreen.kt create mode 100644 app/src/main/java/com/samuel/inventorymanager/screens/OnboardingScreen.kt create mode 100644 app/src/main/java/com/samuel/inventorymanager/screens/ShareScreen.kt create mode 100644 app/src/main/java/com/samuel/inventorymanager/services/AIService.kt create mode 100644 app/src/main/java/com/samuel/inventorymanager/services/AIServicess.kt create mode 100644 app/src/main/java/com/samuel/inventorymanager/services/OCRService.kt create mode 100644 app/src/main/java/com/samuel/inventorymanager/services/Services.kt create mode 100644 app/src/main/java/com/samuel/inventorymanager/viewmodels/AuthViewModel.kt create mode 100644 app/src/main/res/drawable/inventorymanger_background.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/inventorymanger.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/inventorymanger_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/inventorymanger.webp create mode 100644 app/src/main/res/mipmap-hdpi/inventorymanger_foreground.webp create mode 100644 app/src/main/res/mipmap-hdpi/inventorymanger_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/inventorymanger.webp create mode 100644 app/src/main/res/mipmap-mdpi/inventorymanger_foreground.webp create mode 100644 app/src/main/res/mipmap-mdpi/inventorymanger_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/inventorymanger.webp create mode 100644 app/src/main/res/mipmap-xhdpi/inventorymanger_foreground.webp create mode 100644 app/src/main/res/mipmap-xhdpi/inventorymanger_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/inventorymanger.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/inventorymanger_foreground.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/inventorymanger_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/inventorymanger.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/inventorymanger_foreground.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/inventorymanger_round.webp create mode 100644 app/src/main/res/xml/file_paths.xml 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" /> + + - + jY=idGH+iyQN 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

HMaUp_=d{J>M6{D##8# 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 gHFalQVeb=<>a0+&U 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

g|zJ2p-y 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®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 z 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ǨLJ3Rk}L=VGs>Id68jYRDYkWw@>G2 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

a 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%0A3e(Q%Cg^aKd>V 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 literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/samuel/inventorymanager/Activity/ImageEditorActivity.kt b/app/src/main/java/com/samuel/inventorymanager/Activity/ImageEditorActivity.kt new file mode 100644 index 0000000..44c5391 --- /dev/null +++ b/app/src/main/java/com/samuel/inventorymanager/Activity/ImageEditorActivity.kt @@ -0,0 +1,103 @@ +package com.samuel.inventorymanager.screens + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Brightness4 +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Crop +import androidx.compose.material.icons.filled.RotateRight +import androidx.compose.material3.AssistChip +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter + +class ImageEditorActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val uri = Uri.parse(intent.getStringExtra("imageUri")) + setContent { + ImageEditorScreen( + uri = uri, + onSave = { + setResult(Activity.RESULT_OK, Intent().putExtra("editedUri", uri.toString())) + finish() + }, + onCancel = { finish() } + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ImageEditorScreen( + uri: Uri, + onSave: () -> 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 0000000000000000000000000000000000000000..72259b5ce82067d4892820c1fdd14b7355a48ecd GIT binary patch literal 4090 zcmVn-~k|Sj-g5!_sAhdb%zh4Wh{{e8qNSPr=}TXu53{e-AS+Pm>9YE_-q=d5SUD)Qm!a3k5SLm^)hcy#cLxK9(S5}&^RSeb zhJjAMbXZFHn3?ec(i!j!x|YtPHD$5O%#5QqU1`+JygkBKG&jijM>;9yb|yM9B8?Jxj2+Pt zYE#Vcvz{sLNH0Toc#+89&;jHvc|uQ)gGiDjrD}HXKsLhg^+Q35B*~E!kt>cr zki&5(Bmpg6w-b&c*>uPOlX0GpJ_`R;GSo?~$N@=sSeO1Ng#!jWqX6N*_?R0h?!)+)D zV53NqB-s%K!@O|Y|G$bEiz~wx)!MdQ?af;ITty^dH6kV^6md>W!)5%;^|%ismyvUL zXU7Bp(y_Z2O2@0RCsMKg0LS^&ZoprA6ENlW!$R}W7~jc z+#4~Upow2GwhI)vZxHhnrQ*JbvAqSC4SXA(?7T8pMN94poP_}4gMIFA482_UaNEPs zoz<@B6P@T%qbsy7bg0phg+ufJHvofZ5$Hwq<^ZXI3s`gI9ejGVk6zHj{lLHdGh(+O z&(LS}a(ghR_0eOuK;zgsbPDjmb^MgE=mF6fpL6saC*Iaup6>n2u-Ru%Qb@XaRMdO8 z?C!zVM{9cxJ)kqu0FNU?QZpkIAP_djISecY-GHoA$sZ{sh`Bw#3ls;vOfBEQgsmY#xVfra{w*H65u+cZ@A8g zKpGtx=@o*Gqg|$EsFw?STdTCq)WD!v(D0ntqMLARw3w8RX`beZ-*uYElHS4ulCTQY zYXFp*3+Ks4n&-M5aYcHoU=Qt}cbE-uP6F_a<9n#X7)YdPHPz~!0%zjLfCwxM?h2mF zT%n#~-Yy>3JO(^XJoWw{rrHaRO%Rp@_~IwLgcxjE1~3^=dg2_27#B7HWS@$Gth96D zv7@`eHEC7Q1r*Ws={g-QNMyY2o%QQ<{5=3(*`Nd{81{lnVLUK^z;BB%F()ymh|}R4 zVi~(B0t<3jbo=@^LjV^Pq2A|%GcQf+s6DH==7^| z_nM)GU=Dx7H5J8_v8{7bn^dM1CH7#+BkqXhiQgiWf{Y~u)LkLVuuub;3-R@}L)6s_ zT>!HHk{Qv?GqPxE+ZVHln+q_HVA=W)N%-HDzMCpHj>V$1zB@uxwEVUOx%G;W1FYuN;#yN9hXF3>O(`Ve4E>O?_u zKEt4u2o98nnop}W<~Ync3?8Bm8#7myn`RTZ`5mH_-$)5;ZQ27LLkPNw8y1?>x9xSt zEu?U)kHpPL2K|ZCe@Yt-Odkk94PsZnkOG{c_Y~l!L(x$pHT+^md04#}^{_owXcKa5P{G?_w1W;_zuTf z`Dm`VN#hg5aSd!cQCU5uUA|KFk_+$r^q)h}TxDKj^GFF-HjNBDSz(JZ<4}GOvTPi2 zz|63k!heRhc;#|bzmA4iJp8tVzF8$ckvU|M{C6HRy-GrAcO(kAr58Uvl`8|fbg2W$ z5ugZEsG+o&=Ep9%y^d!5^#TmvzT$oDC6M9%ZHi z)(u618tz;%b7rbYr7%H4MJx)WwROliav0n+a5xYiLIXx)^-zH`53|-E!%hM3e>#2M zaQJ6hKFgg?kH4OHT>cR4bjN&HpLX8(Birit<` z(&11UU5n+E^SwsVM`cSS;HXM<3vxzzZK969Q;u{}4g|+rx*rC0knXG^pI&(d3d;3Pdcb zh;{FYB4+?>O@sCHZ6(L5%s5tZgUL8|!Qn9klOmtWbs)e8$Ge#75>PkdGTN6EXG%Zw z_P1`DGzK9BC5(dV2&ZhBDJ8hN7MomST1eNi?DM~k&xnB%U7GhrL9r(t$T3Nv!(1wuIAa`<4B1R>YdRQl45&glEXwhD z{oXLNnyaeqB0)@VvAJv|)PPSKbWSH4A&A~w-M&I@-48max)K6o4+I}b zX3km7%pJ}B$yH|RlgRMe7xjDaUB2|US#iWE1I&l|KdSkFc{w9`l9seg$|?JIiPw@H zvFyp}ra6O{k!Uo+*x_c5T-UT%(w6#K@TU&VG0W-iuiV%HR-r6grc1$2C~p?PQ+@T} z=R1?mB!HaF3+~%I%$0)89tm~*+vT|7E3=jZH{Q6Y1E-?KC@}@*Akn$Wdz&UWwez1$tQ986GV3kxf z7m3V~ehtN7V0;cjDRX52s1Mr%1Yp-DP-o0roddWRFb)751Mnf#eSs@3fZrqxJ{e8Z zk`f8Pv$zcb02~;oZP%-zj)y^${h;1;8DQGr52hef$SfMbF(J968zF0f&u{@BtH%5! z!-XJ^HZ~avs38UqB6~g6aDq&WuPCXcMond1aFFF!QWPwHiiZXAn)JkX@ z)QPNZOOm;J`q4FYO*Lm%z_anpzy>s^;D9)G*PCc)Ysv|&QFrI=bQP7M(=P{$i%(Lk zOJb%>K09mDWfZ5wV5YU>M8>NJ`%Rm_E!#wLoY3dppc!^#`Ob_BsLp{dG2VEwiCA=| zYZ?X^gnPWM517DW&0ul4;VMNz{I7QMt&>S_su&0CC0L@ID7XJDQK0=`x?-dlwK+oo zbs&j}q%$rn+0l}jipow9z#>@W;)FsCP@jcupIx3f%pEyq{5m}92h-Fom+J-dEW*j- zSx6MRVcSZW+Var|ddtzRGKa2a<0<2Ygq&l6IaEv~VTRtErpF#th0>}nuYHm*P>7<`ax^+y3F z#*YneF#FB{C&(8s#^HV&4XU_u-Ey!3f~~pT!tk+N}&YeAX2J7ils|1XZ;cBiYIkpU7Tiq1I zDU1E-;k!S4<}V8R2}`$TAk=&0+$^Fw#uHM#Dz4IJN51ty@D;0bIkHBGag6wquWjUe zA<}3CT^a>~0D)i~p53nrBv1k(VvEDW%=5f-a2B&?%uL(so#6Ja=A&`ZD*zx6gu3nr zb&ibF!?=0U;L6r#WoFBGYr0ZMYW@5Hu5*2Uyk&Uboh`dih-n2RfyI$=vBBDm2#FPa zdG6fx%0GS1pS)?^exuS4nU0MzB-0ZwfW*Q12Ke0>#=X(+IB5eG05rkTCUATc0N4-U zSpYmh0}ng{^QMFU0sjXOb}hdG$`C1qW_n=^%o|>_SIx30t8>#z{u>Au+#wMI!yU4r zt?wW}cC__9$!qKV@B9|QGpBhk;k>eCdO9clGsScUzTMYDO(dXp(HbWDKo6+rwA(X4 s4){&&f7k^vQv2~#Tbutn^QyWg!**Rn`d6TgPR9s{w4FKa{)~=M0r9iCO8@`> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c50c46b26830932d46f95cdd9293938a356c4af9 GIT binary patch literal 8988 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..505a7782ad2d1794a87f1f37ca785c4462cadc14 GIT binary patch literal 6072 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a5bac1c32dc108db49b225af71e6923d1cb8a7ba GIT binary patch literal 2446 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 Meq{Im-sEQ`0LY-N`2YX_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9bec189c129b75bbd2d0f734fcabb4776ca38cc8 GIT binary patch literal 4852 zcmVd zZB^<2aX(9n)y+Q|iU{1D2pC0@q^M&3i#rMRK3v}8=>G(Ojcauussc?yO3=7F_kivV z2KRHP+W@`O8Vh+o11Iicj4d1lVJ=2>P+P5YpRXL*67 zgb*I49c(QQ1#R0%34hxA?*K%^1aMvT2T0X00|g5=?Kb=8NYYcir;l1|O z;)i$_=bU4Dd%7~h>DIRGYERbM=YH}M-u0kCrVelwkkJAaQH>8Fg^)q$y?tlL1O(zs z6buYk;yHNgjq6uj5U~I{49w645CH@e1b&RtbIOndo8pt>edaCkS@V^zB=gjGiNnRH z6o*3JfdV*Ni!gu?mGLqbEAzv6MFW3KSGZ!r+0ASZB8evyR&TQrdYU2#>*gVmY z<~3&wJB7Y{V3rc7Ab|=1kB~tH05zkks!FA>t+N!Jzf5*UX!z;vn$Cuv2;fAVmRoJ(Ad8 zLWzrLWer^X*|&3V2RD-lBtUd_0f3CELcfBj2%tulhy(_{vM-2=pn?LB0MrT?<;8h$ zcvP?P|k_`n_+{-psA`Lk-FLS(D^FI{^*rk ze*WWa*&H}CG0RLsnyZ3a-AHvihl~5^xlWCEdMMLUMO9EUICb@b!POGc2p(6B+QT** zgYMdNZzOL_%XbCyg%f^qZ*JTzvM@6@NZnoKiVPaSlNOIw?-w8T$Ez%*#O


Lhognws++9w-mNX#D(t>Y?<1V;q}lU;;rT1KX#a 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 ayo##caVsEzy6ypPgAT>lK80I+QFQ>8s3)2L literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..07eda009405eb5387e47c92f7cd62a6b5e0391fc GIT binary patch literal 3390 zcmV-E4Z-qKNk&FC4FCXFMM6+kP&iB~4FCWyFTe{BO)zZRHgXWE-B))J{f`7__P17t z`aA5^0;Gjxp^%XzsSwtmaPMJg4hD@RDJlU+GS0suc;=U&sBI+2I{qLR!1ey@e*i1K z06_27&ff(}Z)Z=fYLqQyX1f}?^66&n+6ACs`{&dOZ74|FfNA~9-q8>d6TsAF;Yd2u z|F4al?@tTf4Kp)Cxouz%tE9@LO_?e!bqDRefb`5<@%pMTGc$9R*V6A>EWd;AfvuqJ zz#jf*XRJA9=9{6LZQHW9Iq&BaILzGP2C1zjr%<~_uOD&VI9GE@yl{0-2Ervgv`N-2Toy?;OVY&zSv8M3XrzUt*; z+qP{+&TL!Z2^}Ha(Q&#@gkP|2YNCx?3-xr*cl zcxcXL!zB{VU&RZTU!&#uWd(`` z2J#D+BU+yCiaW;@Xc}Ga_mSV7W?(2u$2jbE;9>0E6UNol%y(||fW9Ps`{b3b)u#&7 zQJB6X@*c)K>C;`L3JY zU9FAO!@HwAPctqa?>6i9@jWbe6PFlvBWOSp3L!A`jY~~`96z{N49w^Uw?=@$hydvf zXH^p}b}9F}cPGg!p4<=sSTxku&jJ;f889RaNwTsN@$E8byu|-q= zxOxGT`-u}gz@jp#R)aB?si@TkS|-VAMY`HDID~Am0_r(ng${l2yF5I}H!fy4FB!2^ zpEQr1`WIJAQcMF0BZ$L1fs!eFQk@aP0AjIIDFljiwWJiKAziF= zDVjyVdPk{_%;n``i>^CJ#F#;N?py3kWe5ZmMyCl?JjuCvHFtLb~!tnG|eOzo!dyX zB%wo9VUVSQFf;y=|U23W#mnsAv$3Hl)>Sh4c$itz45xnLVD!BPqjR&3ADx}l}by|-o7Tsvp4(gH& z%e8iZ%Xeo*z{&)x*)WG{NfsjM@GhZ>P0g=UPq}$^-my`N4EtT_nE%&KP5Pj^)KF8L zmHLaH{#<^(8j>J^A2g%@7%lKbVPox6_x!M2FL+cW0d@AxT_?(WB+m$E1CoSbS+jxA z2R>g`I8e^iSu({sz9lRZz-B9=Yk+Ti7YSqjP%pKtu~?@|nLz;Gw4s6%4 zCDL;CzI8K_QoVTg>#g`&1xgaTY#@>YnhG6B`SQp6`$M(uKU8Vv+iyEyHlBKQ8JQSl z>YwXiTRZNlKap|w&^fg*KGJNxL;7vQi+y%oG2TF~0$1~LFaMt1aaj7C+C;=d>wrz(K>zq z-S6|y#c#j6uDmpDRQq@Qm*zuWT4J(QTm*U+Nvr@9An01IlRMu3^QV}H-N%IUedwyM zzB?ZJl8UbiRuoK;G!{qQ89w+g>MoN!dKg$1y%3bA@#)WmcVDah{yY5B57YU7!s z*Crl#`^-}!L%Gk8qKF;;sA6~=Uz`!G=iZ2R!w3w@80~V zsh2!fd`UjR7*#cTrlBX5^uc-piKCb6dmOx78NgQwK1*K z*xDEhhXs9s!{T(xDUh$*&p(U%KYZ<-BM&VMORX7-n8+57p$L+CB=L>{K%zjykLm;< z14Vb;V#?}4l@Jyz_)}w>+2&CsE}XD7r2+{pKuUs!4X(XKkcC33VB`i`q?4qCt+hgJPeW$B%v%XN0(fhyBL`8EHE>`adU1)(8}-lp*jz z#E7@_-gx}~9zZwVdvUgq{7!*JmIeJ;E>bJ@cIp-W1@(e@sMHwO!mUwK=>tLGjkS`} zV`BPf0xc0OC{ct=lF-P!`SbplSAAeMzb(2geRu225Q0-sS>aDaNg|pc3X2Yqm82fz z759$Z?RrPz`+^~A$$>CpY5{`X3KdYlii@~>1q|A5+a3JtwX~&uvL*nD(jWq(qT~?@ zL}{u-6Q3=BvyqhTCuc1k1n1;59Z@f;1|vXHWhaGFK^kHT1-%X!9{j`g)bh}m3bfB= zvBWX$OdeYH7c@x_?G85EAVDSaAm}hq-;##w;tWfhMzar%N>T}c`Z|$n2CMbAD9u77 zL$9yBq}u+}Hw|~5RNcfcllGdxRn@5?NhmNt;?Kftwj>cjkF2Y7Q)pLf0aXAo$ZCuToAf!1 z#k3e(_>DrV)j#+~CR&}k;<&Yacz=F(nkDS^0SuduIk;h~XF{SXOB!3G&61SJ05%yY zETLFc1r-3lK@gIZ3M(mLU6#9Bk2m44dD!hk(hMgt;nUstVMm*Lm^$9sU-I(4V7cHy3zfgF65#Q8_mk=)xSzoMGf&5 zU_W4JCHK?xvxhMqV@DpXFSKwv8%z~zvJMKa-M)Hb>7orNR+tV_gBxaqvZmH9+E}>{ zd&WjaH`Mw^SJq}k1bDmU_~6^(wBYvYgF}Ek7PLk6)FZA|7w69ygQL)(z(llpKI>>C zjj@4optxq6-dbRE#kAS|QEr@jc;3v)v%h9me07b?J9CSXa_P9u%sXylLc1h485j9Q+{Y`%1}saG87Ws@SP}@iRMNl1xFu<@r@e#Mgb++ z{E@N{tjWn#@zD0ks0b>ik|OdTg55}=Sf6ARLtaKMB*qWGP{Ri*(12Y%=K8{x%??De z)!Anh(_xY}YQQlQsxmfaRI8axUKPUT-UAz!0j)YbRp9|(u5WFvXMS3k!~1pWMw>b_DtlTYl~4##Q3zQ1 zR{yiTp2pnZ0S*JyV_qb-J8K#(a2Z1e_7i04ko<2OTfhH)vJ&C|1f#>Uq9}4P#IH4 z#=?8|-tT{ZdT|z*Td-h3+pGHl_dge)YE*03ouvGmr}?}8FUOnD#q^gRG@u(FSFJX0 U`dhEkWtX2@`xCa-4^wIyF2SH?e*gdg literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cbbfbd8340651b6b05e4e9cccc03783c35131a08 GIT binary patch literal 6282 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 A+W-In literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..eaa08dbeda815c5f48106d3be2ec0f54ada62bac GIT binary patch literal 13650 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9f2b7854dc2da6d00b5719560cb07cbd88bc7060 GIT binary patch literal 8820 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d363c3a14b505a5ee6894b7127a02094afd2b4b4 GIT binary patch literal 10982 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 Uni2rg(9rPVFBA%cdw+O9DRv#0`~Uy| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ff05c79a86972510641ab08abf0fa54a42ad181d GIT binary patch literal 26874 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 FH2|`3sRsZ6 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..aa6a30e7c6bb0d92e3891431f006e62b3a4a2de7 GIT binary patch literal 15274 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..356ec5b04b31b7dac58c9b4e74006f0e7f1f20ce GIT binary patch literal 47078 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 NU5&8NRCX$dE&!ZCE$jdQ literal 0 HcmV?d00001 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