diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a4f2e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +local.properties +*.apk +.env +*.keystore +.idea/ +build/ +app/build/ +.gradle/ diff --git a/PRD.MD b/PRD.MD new file mode 100644 index 0000000..dead21d --- /dev/null +++ b/PRD.MD @@ -0,0 +1,149 @@ +# 📄 Product Requirement Document (PRD): AuraTry AR (Android Native - Jetpack Compose) + +> **Project Name:** AuraTry AR +> **Platform:** Android Native (Kotlin + Jetpack Compose) +> **Target Device:** Android (Optimized for iQOO / MediaTek / Snapdragon devices) +> **Target Build Time:** < 45 Minutes + +--- + +## 1. Executive Summary + +**AuraTry AR** is a native Android application that provides a live digital try-on experience on stage. The app uses **CameraX** to render a full-screen live viewfinder, overlays transparent 2D clothing assets directly over the user's torso, triggers native Android haptics, and calls **MiniMax M3** via **OpenRouter** to synthesize an instant, spoken AI stylist commentary using Android's native `TextToSpeech` engine. + +--- + +## 2. Technical Stack & Dependencies + +* **UI Framework:** Jetpack Compose + Material3 +* **Camera Library:** AndroidX CameraX (`androidx.camera.camera2`, `androidx.camera.view.PreviewView`) +* **Networking:** Ktor Client or OkHttp3 + `kotlinx.serialization` (JSON parsing) +* **Audio & Haptics:** `android.speech.tts.TextToSpeech` & `android.os.VibratorManager` / `Vibrator` +* **Image Loading:** Coil for Compose (`io.coil-kt:coil-compose`) + +--- + +## 3. Core Functional Requirements + +### 3.1 Live Camera Viewfinder (`Feature_01`) + +* Implement a full-screen CameraX `Preview` pipeline rendered inside Jetpack Compose using `AndroidView(factory = { PreviewView(it) })`. +* Force selector to use the rear camera (`CameraSelector.DEFAULT_BACK_CAMERA`). + +### 3.2 Dynamic AR Outfit Canvas (`Feature_02`) + +* Render a transparent PNG asset centered over the torso viewport area using a Compose `Box` layout. +* Implement a bottom horizontal `LazyRow` (Outfit Carousel) displaying available clothing items (`R.drawable.outfit_jacket`, `R.drawable.outfit_hoodie`). +* Highlight the active outfit card with a neon border (`Color(0xFFFF0055)`). + +### 3.3 OpenRouter MiniMax M3 Integration (`Feature_03`) + +When the user taps an outfit card in the `LazyRow`: + +1. Trigger an asynchronous HTTP `POST` request to `[https://openrouter.ai/api/v1/chat/completions](https://openrouter.ai/api/v1/chat/completions)`. +2. Model target: `minimax/minimax-m3`. +3. Pass `extra_body` with `reasoning: { "effort": "low" }` to guarantee sub-800ms generation speeds. + +#### Request JSON Payload: + +```json +{ + "model": "minimax/minimax-m3", + "extra_body": { + "reasoning": { "effort": "low" } + }, + "messages": [ + { + "role": "system", + "content": "You are an energetic, funny AI fashion stylist at a live tech hackathon. Return ONLY valid JSON with key 'pitch' containing a 1-sentence hilarious pitch under 12 words." + }, + { + "role": "user", + "content": "The user just selected: [OUTFIT_NAME]. Hype it up for the stage!" + } + ], + "response_format": { "type": "json_object" }, + "max_tokens": 60, + "temperature": 0.8 +} + +``` + +### 3.4 Audio & Haptics Feedback (`Feature_04`) + +* **Haptics:** Trigger `VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE)` on item click. +* **Text-To-Speech:** Pass the parsed `pitch` string to `TextToSpeech.speak(pitch, TextToSpeech.QUEUE_FLUSH, null, "UtteranceId")`. + +--- + +## 4. UI Architecture & Jetpack Compose Layout + +``` +Box(modifier = Modifier.fillMaxSize()) { + // 1. CameraX PreviewView (Full Screen Background) + AndroidView(factory = { previewView }, modifier = Modifier.fillMaxSize()) + + // 2. Translucent AR HUD Header + Surface( + modifier = Modifier.align(Alignment.TopCenter).padding(top = 48.dp), + color = Color(0xCC0A0A0F), + shape = RoundedCornerShape(16.dp) + ) { + Column { Text(text = "⚡ AuraTry AR Stylist"); Text(text = aiPitchText) } + } + + // 3. AR Clothing Overlay Layer + Image( + painter = painterResource(id = selectedOutfit.drawableRes), + contentDescription = null, + modifier = Modifier.align(Alignment.Center).size(300.dp) + ) + + // 4. Outfit Carousel Selector + LazyRow( + modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 32.dp) + ) { + items(outfitList) { outfit -> OutfitCard(outfit) } + } +} + +``` + +--- + +## 5. Master Implementation Prompt for Coding Agent + +Copy and paste the box below into Cursor, Claude Code, or Android Studio AI Assistant: + +```text +Build a native Android application in Kotlin using Jetpack Compose for "AuraTry AR" (AI Live Outfit Try-On). + +Key Technical Requirements: +1. CAMERAX PREVIEW: + - Use CameraX to display a full-screen live rear camera feed using AndroidView and PreviewView. + - Handle camera permissions gracefully with Accompanist Permissions or ActivityResultContracts. + +2. AR OVERLAY & CAROUSEL: + - Create a data class Outfit(val id: String, val name: String, val drawableRes: Int). + - Display a Box containing an Image overlay positioned at the center of the screen displaying the current selected outfit drawable. + - At the bottom, render a LazyRow showing outfit thumbnails. Apply a neon pink border (0xFFFF0055) around the selected item. + +3. OPENROUTER MINIMAX M3 API INTEGRATION: + - Create a repository function `fetchStylistPitch(outfitName: String): String` using OkHttp or Ktor. + - Target URL: "https://openrouter.ai/api/v1/chat/completions" + - Headers: "Authorization: Bearer ", "Content-Type: application/json". + - Body JSON: Send model "minimax/minimax-m3", extra_body: { reasoning: { effort: "low" } }, response_format: { type: "json_object" }. + - Prompt system: "You are a hilarious AI stylist. Return JSON: {\"pitch\": \"1-sentence hype line under 12 words\"}". + +4. NATIVE TTS & HAPTICS: + - Initialize android.speech.tts.TextToSpeech in MainActivity. + - On selecting an outfit: + a) Trigger a heavy haptic vibration using Vibrator / VibrationEffect. + b) Fetch the pitch from OpenRouter asynchronously using Coroutines. + c) Speak the pitch out loud using tts.speak(). + +5. UI DESIGN: + - Dark Cyberpunk glassmorphic HUD card at top (Color: 0xCC0A0A0F, Border: 0xFF00FFCC). + - Clean, production-ready, single-activity implementation. + +``` \ No newline at end of file diff --git a/README.md b/README.md index ac214cd..133c17f 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ | | | |---|---| -| **Team name** | _e.g. Team Nova_ | -| **Members** | _Name 1, Name 2, Name 3_ | -| **City / Venue** | _Pune / Hyderabad / Bengaluru / Chennai_ | +| **Team name** | AuraTry Team | +| **Members** | Ayush | +| **City / Venue** | Remote | --- @@ -19,17 +19,17 @@ | | | |---|---| -| **App name** | _Your app name_ | -| **Theme** | _Pick one:_ Simple game · Utility app · Productivity · Fun & social · Learning tool · Creative tool | -| **One-liner** | _What your app does, in one sentence._ | +| **App name** | AuraTry AR | +| **Theme** | Fun & social | +| **One-liner** | A live digital AR try-on experience with an AI stylist commentary. | ### What we built -_A short paragraph: what the app does and who it's for._ +AuraTry AR is a native Android application that provides a live digital try-on experience on stage. It overlays AR clothing on the user's camera feed and calls an AI model to generate a funny stylist pitch when an outfit is selected, which is then spoken out loud using Text-To-Speech alongside haptic feedback. ### How the AI is used -- **Model:** _e.g. `openai/gpt-4o-mini` (via OpenRouter)_ -- **What the AI does:** _e.g. generates quiz questions from a topic the user types._ -- **AI pattern:** _Chat · Summarise · Classify · Generate · Extract · Vision_ +- **Model:** `minimax/minimax-m3` (via OpenRouter) +- **What the AI does:** Acts as an energetic, funny AI fashion stylist at a live tech hackathon to generate a 1-sentence hilarious pitch under 12 words for the selected outfit. +- **AI pattern:** Generate --- @@ -71,8 +71,8 @@ cd TechQuest --- ## ✅ Submission checklist -- [ ] This README is filled in (team, theme, how to run) -- [ ] The API key is **NOT** in the repo (see `.gitignore` below) +- [x] This README is filled in (team, theme, how to run) +- [x] The API key is **NOT** in the repo (see `.gitignore` below) - [ ] Final code pushed to **your fork** - [ ] APK and/or a screen recording added or linked - [ ] **Pull Request opened** from your fork → `Reskilll/TechQuest` **before the deadline** diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..20b002c --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,98 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +import java.util.Properties +import java.io.FileInputStream + +val localProperties = Properties() +val localPropertiesFile = rootProject.file("local.properties") +if (localPropertiesFile.exists()) { + localProperties.load(FileInputStream(localPropertiesFile)) +} +val openRouterApiKey = localProperties.getProperty("OPENROUTER_API_KEY") ?: "\"dummy_key\"" + +android { + namespace = "com.techquest.auratryar" + compileSdk = 34 + + defaultConfig { + applicationId = "com.techquest.auratryar" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + val safeApiKey = if (openRouterApiKey.startsWith("\"")) openRouterApiKey else "\"$openRouterApiKey\"" + buildConfigField("String", "OPENROUTER_API_KEY", safeApiKey) + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + compose = true + buildConfig = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.4" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("androidx.activity:activity-compose:1.8.2") + implementation(platform("androidx.compose:compose-bom:2023.10.01")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + + // CameraX + val camerax_version = "1.3.1" + implementation("androidx.camera:camera-core:${camerax_version}") + implementation("androidx.camera:camera-camera2:${camerax_version}") + implementation("androidx.camera:camera-lifecycle:${camerax_version}") + implementation("androidx.camera:camera-view:${camerax_version}") + + // Accompanist for Permissions + implementation("com.google.accompanist:accompanist-permissions:0.34.0") + + // Networking - OkHttp + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + + // Image Loading + implementation("io.coil-kt:coil-compose:2.5.0") + + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01")) + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e3696a9 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/techquest/auratryar/MainActivity.kt b/app/src/main/java/com/techquest/auratryar/MainActivity.kt new file mode 100644 index 0000000..515ea1a --- /dev/null +++ b/app/src/main/java/com/techquest/auratryar/MainActivity.kt @@ -0,0 +1,462 @@ +package com.techquest.auratryar + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager +import android.speech.tts.TextToSpeech +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.CameraSelector +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import java.util.Locale +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +// Data Class for Outfits +data class Outfit(val id: String, val name: String, val drawableRes: Int) + +class MainActivity : ComponentActivity(), TextToSpeech.OnInitListener { + + private lateinit var tts: TextToSpeech + private var isTtsInitialized = false + private val client = OkHttpClient() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Initialize TextToSpeech + tts = TextToSpeech(this, this) + + val requestPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (isGranted) { + setupUI() + } else { + Log.e("AuraTry", "Camera permission denied") + } + } + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { + setupUI() + } else { + requestPermissionLauncher.launch(Manifest.permission.CAMERA) + } + } + + private fun setupUI() { + val outfits = listOf( + Outfit("1", "Cyber Jacket", R.drawable.outfit_jacket), + Outfit("2", "Neon Hoodie", R.drawable.outfit_hoodie) + ) + + setContent { + MaterialTheme { + AuraTryScreen( + outfits = outfits, + onOutfitSelected = { outfit -> + triggerHapticFeedback() + fetchAndSpeakPitch(outfit.name) + } + ) + } + } + } + + override fun onInit(status: Int) { + if (status == TextToSpeech.SUCCESS) { + val result = tts.setLanguage(Locale.US) + if (result != TextToSpeech.LANG_MISSING_DATA && result != TextToSpeech.LANG_NOT_SUPPORTED) { + isTtsInitialized = true + } + } + } + + private fun triggerHapticFeedback() { + val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibratorManager = getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager + vibratorManager.defaultVibrator + } else { + @Suppress("DEPRECATION") + getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + } + + if (vibrator.hasVibrator()) { + val effect = VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE) + vibrator.vibrate(effect) + } + } + + private fun fetchAndSpeakPitch(outfitName: String) { + CoroutineScope(Dispatchers.IO).launch { + try { + // Ensure you set your API Key securely in production + val apiKey = BuildConfig.OPENROUTER_API_KEY + + val jsonBody = JSONObject().apply { + put("model", "minimax/minimax-m3") + val reasoning = JSONObject().apply { put("effort", "low") } + put("extra_body", JSONObject().apply { put("reasoning", reasoning) }) + + val messages = org.json.JSONArray() + messages.put(JSONObject().apply { + put("role", "system") + put("content", "You are an energetic, funny AI fashion stylist at a live tech hackathon. Return ONLY valid JSON with key 'pitch' containing a 1-sentence hilarious pitch under 12 words.") + }) + messages.put(JSONObject().apply { + put("role", "user") + put("content", "The user just selected: $outfitName. Hype it up for the stage!") + }) + put("messages", messages) + + put("response_format", JSONObject().apply { put("type", "json_object") }) + put("max_tokens", 60) + put("temperature", 0.8) + } + + val requestBody = jsonBody.toString().toRequestBody("application/json".toMediaType()) + + val request = Request.Builder() + .url("https://openrouter.ai/api/v1/chat/completions") + .addHeader("Authorization", "Bearer $apiKey") + .post(requestBody) + .build() + + val response = client.newCall(request).execute() + if (response.isSuccessful) { + val responseBody = response.body?.string() + responseBody?.let { + val responseJson = JSONObject(it) + val contentString = responseJson.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content") + + // Parse inner JSON + val innerJson = JSONObject(contentString) + val pitch = innerJson.optString("pitch", "Looking fresh!") + + withContext(Dispatchers.Main) { + if (isTtsInitialized) { + tts.speak(pitch, TextToSpeech.QUEUE_FLUSH, null, "PitchUtterance") + } + } + } + } else { + Log.e("AuraTry", "API call failed: ${response.code}") + } + } catch (e: Exception) { + Log.e("AuraTry", "Error fetching pitch", e) + } + } + } + + override fun onDestroy() { + if (::tts.isInitialized) { + tts.stop() + tts.shutdown() + } + super.onDestroy() + } +} + +@Composable +fun AuraTryScreen(outfits: List, onOutfitSelected: (Outfit) -> Unit) { + var selectedOutfit by remember { mutableStateOf(outfits.first()) } + var aiPitchText by remember { mutableStateOf("Ready to scan...") } + + // Colors based on OriginOS Premium Speedtech + val Obsidian = Color(0xFF0A0A0F) + val CharcoalGlass = Color(0xBF14141E) + val Cyan = Color(0xFF00FFCC) + val Crimson = Color(0xFFFF0055) + val Gold = Color(0xFFFFD700) + val Titanium = Color(0xFF8A8A9E) + + Box(modifier = Modifier.fillMaxSize().background(Obsidian)) { + // 1. CameraX PreviewView + CameraPreviewView(modifier = Modifier.fillMaxSize()) + + // Dark Overlay for Camera Stream + Box(modifier = Modifier.fillMaxSize().background(Obsidian.copy(alpha = 0.4f))) + + // 2. Telemetry Grid Layer + Box(modifier = Modifier.fillMaxSize().padding(24.dp)) { + // REC Indicator + Row(modifier = Modifier.align(Alignment.TopStart), verticalAlignment = Alignment.CenterVertically) { + Box(modifier = Modifier.size(6.dp).background(Cyan, androidx.compose.foundation.shape.CircleShape)) + Spacer(modifier = Modifier.width(8.dp)) + Text("REC", color = Cyan, fontSize = 10.sp, fontWeight = FontWeight.Bold, letterSpacing = 2.sp) + } + + // FPS Counter + Text( + "AI NPU 60 FPS", + modifier = Modifier.align(Alignment.TopEnd), + color = Cyan, + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + letterSpacing = 2.sp + ) + + // P2P and AR Status + Text( + "P2P SYNC 12ms", + modifier = Modifier.align(Alignment.BottomStart), + color = Titanium, + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + letterSpacing = 2.sp + ) + Text( + "AR: ON", + modifier = Modifier.align(Alignment.BottomEnd), + color = Titanium, + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + letterSpacing = 2.sp + ) + } + + // 3. Center AR Reticle & Clothing Overlay Layer + Box( + modifier = Modifier + .align(Alignment.Center) + .size(280.dp, 380.dp), + contentAlignment = Alignment.Center + ) { + // Corner brackets + Canvas(modifier = Modifier.fillMaxSize()) { + val bracketSize = 24.dp.toPx() + val strokeWidth = 2.dp.toPx() + val color = Cyan.copy(alpha = 0.6f) + + // Top Left + drawLine(color, androidx.compose.ui.geometry.Offset(0f, 0f), androidx.compose.ui.geometry.Offset(bracketSize, 0f), strokeWidth) + drawLine(color, androidx.compose.ui.geometry.Offset(0f, 0f), androidx.compose.ui.geometry.Offset(0f, bracketSize), strokeWidth) + // Top Right + drawLine(color, androidx.compose.ui.geometry.Offset(size.width, 0f), androidx.compose.ui.geometry.Offset(size.width - bracketSize, 0f), strokeWidth) + drawLine(color, androidx.compose.ui.geometry.Offset(size.width, 0f), androidx.compose.ui.geometry.Offset(size.width, bracketSize), strokeWidth) + // Bottom Left + drawLine(color, androidx.compose.ui.geometry.Offset(0f, size.height), androidx.compose.ui.geometry.Offset(bracketSize, size.height), strokeWidth) + drawLine(color, androidx.compose.ui.geometry.Offset(0f, size.height), androidx.compose.ui.geometry.Offset(0f, size.height - bracketSize), strokeWidth) + // Bottom Right + drawLine(color, androidx.compose.ui.geometry.Offset(size.width, size.height), androidx.compose.ui.geometry.Offset(size.width - bracketSize, size.height), strokeWidth) + drawLine(color, androidx.compose.ui.geometry.Offset(size.width, size.height), androidx.compose.ui.geometry.Offset(size.width, size.height - bracketSize), strokeWidth) + } + + // Center Crosshair + Box(modifier = Modifier.size(8.dp).background(Cyan.copy(alpha = 0.5f), androidx.compose.foundation.shape.CircleShape)) + + // AR Asset Placeholder + Image( + painter = painterResource(id = selectedOutfit.drawableRes), + contentDescription = null, + modifier = Modifier.size(260.dp) + ) + } + + // 4. Translucent AR HUD Header + Surface( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 56.dp, start = 16.dp, end = 16.dp), + color = CharcoalGlass, + shape = RoundedCornerShape(28.dp), + border = androidx.compose.foundation.BorderStroke(1.dp, Cyan.copy(alpha = 0.4f)) + ) { + Column( + modifier = Modifier.padding(20.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box(modifier = Modifier.size(16.dp, 16.dp).background(Cyan)) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "AURATRY AR // STYLIST", + color = Titanium, + fontWeight = FontWeight.Bold, + fontSize = 12.sp, + letterSpacing = 1.5.sp + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .background(Cyan.copy(alpha = 0.1f), RoundedCornerShape(12.dp)) + .border(1.dp, Cyan.copy(alpha = 0.3f), RoundedCornerShape(12.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Box(modifier = Modifier.size(6.dp).background(Cyan, androidx.compose.foundation.shape.CircleShape)) + Spacer(modifier = Modifier.width(6.dp)) + Text("NPU ACTIVE", color = Cyan, fontSize = 9.sp, fontWeight = FontWeight.Bold, letterSpacing = 1.sp) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + Row(crossAxisAlignment = CrossAxisAlignment.Start) { + Text("✨", fontSize = 20.sp, modifier = Modifier.padding(top = 2.dp)) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = aiPitchText, + color = Color.White, + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 20.sp + ) + } + } + } + + // 5. Outfit Carousel Selector (OriginOS Component Dock) + LazyRow( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 48.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(horizontal = 24.dp) + ) { + items(outfits) { outfit -> + OutfitCard( + outfit = outfit, + isSelected = selectedOutfit.id == outfit.id, + onClick = { + selectedOutfit = outfit + aiPitchText = "Scanning matrix vectors..." + onOutfitSelected(outfit) + } + ) + } + } + } +} + +@Composable +fun OutfitCard(outfit: Outfit, isSelected: Boolean, onClick: () -> Unit) { + val Crimson = Color(0xFFFF0055) + val Cyan = Color(0xFF00FFCC) + val CharcoalGlass = Color(0xBF14141E) + val Titanium = Color(0xFF8A8A9E) + + val borderColor = if (isSelected) Crimson else Cyan.copy(alpha = 0.2f) + val yOffset = if (isSelected) (-12).dp else 0.dp + val opacity = if (isSelected) 1f else 0.6f + + Box( + modifier = Modifier + .offset(y = yOffset) + .size(100.dp, 130.dp) + .clip(RoundedCornerShape(20.dp)) + .background(CharcoalGlass) + .border(if (isSelected) 2.dp else 1.dp, borderColor, RoundedCornerShape(20.dp)) + .clickable { onClick() } + .padding(12.dp), + contentAlignment = Alignment.BottomCenter + ) { + if (isSelected) { + Box( + modifier = Modifier + .align(Alignment.TopCenter) + .offset(y = (-20).dp) + .background(Crimson, RoundedCornerShape(10.dp)) + .padding(horizontal = 10.dp, vertical = 4.dp) + ) { + Text("ACTIVE", color = Color.White, fontSize = 9.sp, fontWeight = FontWeight.Bold, letterSpacing = 1.5.sp) + } + } + + Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .size(72.dp) + .background(Color.White.copy(alpha = if (isSelected) 0.15f else 0.05f), RoundedCornerShape(16.dp)) + .border(1.dp, Color.White.copy(alpha = 0.1f), RoundedCornerShape(16.dp)) + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = outfit.name, + color = if (isSelected) Color.White else Titanium, + fontSize = 12.sp, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium, + maxLines = 1 + ) + } + } +} + +@Composable +fun CameraPreviewView(modifier: Modifier = Modifier) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + AndroidView( + factory = { ctx -> + val previewView = PreviewView(ctx) + val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx) + + cameraProviderFuture.addListener({ + val cameraProvider = cameraProviderFuture.get() + val preview = Preview.Builder().build().also { + it.setSurfaceProvider(previewView.surfaceProvider) + } + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + + try { + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle( + lifecycleOwner, + cameraSelector, + preview + ) + } catch (e: Exception) { + Log.e("AuraTry", "Camera bind failed", e) + } + }, ContextCompat.getMainExecutor(ctx)) + + previewView + }, + modifier = modifier + ) +} diff --git a/app/src/main/res/drawable/outfit_hoodie.xml b/app/src/main/res/drawable/outfit_hoodie.xml new file mode 100644 index 0000000..15357fb --- /dev/null +++ b/app/src/main/res/drawable/outfit_hoodie.xml @@ -0,0 +1 @@ + diff --git a/app/src/main/res/drawable/outfit_jacket.xml b/app/src/main/res/drawable/outfit_jacket.xml new file mode 100644 index 0000000..5888c8b --- /dev/null +++ b/app/src/main/res/drawable/outfit_jacket.xml @@ -0,0 +1 @@ + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..ca1931b --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..80461e7 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + AuraTry AR + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..4baea3e --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +