diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bac5fb0..1791e99 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Build and Release IPA +name: Build and Release on: push: @@ -9,7 +9,7 @@ permissions: contents: write jobs: - build: + ios: runs-on: macos-latest steps: - uses: actions/checkout@v5 @@ -27,5 +27,27 @@ jobs: uses: softprops/action-gh-release@v3 with: files: | - build/Milliways-unsigned.ipa - build/Milliways-simulator.zip + ios/build/Milliways-unsigned.ipa + ios/build/Milliways-simulator.zip + + android: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Set up JDK 22 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '22' + + - name: Make gradlew executable + run: chmod +x android/gradlew + + - name: Build debug APK + run: make apk + + - name: Upload artifact to release + uses: softprops/action-gh-release@v3 + with: + files: android/app/build/outputs/apk/debug/app-debug.apk diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index b27b076..aa00e6f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -26,7 +26,7 @@ jobs: uses: actions/upload-artifact@v5 with: name: ipa-build - path: build/Milliways-unsigned.ipa + path: ios/build/Milliways-unsigned.ipa test: runs-on: ubuntu-latest @@ -40,7 +40,7 @@ jobs: uses: actions/download-artifact@v5 with: name: ipa-build - path: build + path: ios/build - name: Run tests using mobilewright working-directory: tests diff --git a/Makefile b/Makefile index 9fad8f5..e1a6c36 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ -.PHONY: all zip ipa clean +.PHONY: all zip ipa apk clean -all: zip ipa +all: zip ipa apk zip: $(MAKE) -C ios zip @@ -8,6 +8,9 @@ zip: ipa: $(MAKE) -C ios ipa +apk: + $(MAKE) -C android build + clean: $(MAKE) -C ios clean $(MAKE) -C android clean diff --git a/android/Makefile b/android/Makefile index e69de29..334d3c1 100644 --- a/android/Makefile +++ b/android/Makefile @@ -0,0 +1,15 @@ +GRADLEW = ./gradlew +APK_PATH = app/build/outputs/apk/debug/app-debug.apk + +.PHONY: build lint clean + +build: + $(GRADLEW) assembleDebug + @echo "Created $(APK_PATH)" + +lint: + $(GRADLEW) lint + @echo "Lint report: app/build/reports/lint-results-debug.html" + +clean: + $(GRADLEW) clean diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index cc523cd..d7fbc25 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -50,6 +50,8 @@ dependencies { implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.navigation.compose) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2c94e9d..f8bcad9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -15,7 +15,8 @@ android:name=".MainActivity" android:exported="true" android:label="@string/app_name" - android:theme="@style/Theme.Milliways"> + android:theme="@style/Theme.Milliways" + android:windowSoftInputMode="adjustResize"> diff --git a/android/app/src/main/java/com/mobilenext/Milliways/MainActivity.kt b/android/app/src/main/java/com/mobilenext/Milliways/MainActivity.kt index 218ea17..00fff71 100644 --- a/android/app/src/main/java/com/mobilenext/Milliways/MainActivity.kt +++ b/android/app/src/main/java/com/mobilenext/Milliways/MainActivity.kt @@ -4,44 +4,115 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically +import androidx.compose.runtime.remember +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.mobilenext.Milliways.model.OrderManager +import com.mobilenext.Milliways.ui.screens.DeliveryScreen +import com.mobilenext.Milliways.ui.screens.MenuScreen +import com.mobilenext.Milliways.ui.screens.OrderScreen +import com.mobilenext.Milliways.ui.screens.WelcomeScreen import com.mobilenext.Milliways.ui.theme.MilliwaysTheme +private const val ROUTE_WELCOME = "welcome" +private const val ROUTE_MENU = "menu" +private const val ROUTE_ORDER = "order" +private const val ROUTE_DELIVERY = "delivery" + +private const val TRANSITION_MS = 350 + class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { MilliwaysTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Greeting( - name = "Android", - modifier = Modifier.padding(innerPadding) - ) + val navController = rememberNavController() + val orderManager = remember { OrderManager() } + + NavHost( + navController = navController, + startDestination = ROUTE_WELCOME, + // iOS-style push: new screen slides in from the right, current + // screen slides out partway to the left (and reverses on pop). + enterTransition = { + slideInHorizontally(tween(TRANSITION_MS)) { it } + }, + exitTransition = { + slideOutHorizontally(tween(TRANSITION_MS)) { -it / 3 } + }, + popEnterTransition = { + slideInHorizontally(tween(TRANSITION_MS)) { -it / 3 } + }, + popExitTransition = { + slideOutHorizontally(tween(TRANSITION_MS)) { it } + } + ) { + composable( + ROUTE_WELCOME, + // Closing Delivery pops straight back to Welcome (skipping Menu/Order) β€” + // let the cover's own slide-down read as the transition, Welcome just stays put. + popEnterTransition = { + if (initialState.destination.route == ROUTE_DELIVERY) { + EnterTransition.None + } else { + slideInHorizontally(tween(TRANSITION_MS)) { -it / 3 } + } + } + ) { + WelcomeScreen( + orderManager = orderManager, + onNewOrder = { navController.navigate(ROUTE_MENU) } + ) + } + composable(ROUTE_MENU) { + MenuScreen( + orderManager = orderManager, + onBack = { navController.popBackStack() }, + onViewOrder = { navController.navigate(ROUTE_ORDER) } + ) + } + composable( + ROUTE_ORDER, + // Order stays static underneath while Delivery slides up as a cover. + exitTransition = { ExitTransition.None } + ) { + OrderScreen( + orderManager = orderManager, + onBack = { navController.popBackStack() }, + onPlaceOrder = { navController.navigate(ROUTE_DELIVERY) } + ) + } + composable( + ROUTE_DELIVERY, + // iOS fullScreenCover: slides up from the bottom over the + // current screen, and slides back down on close. + enterTransition = { + slideInVertically(tween(TRANSITION_MS)) { it } + }, + exitTransition = { ExitTransition.None }, + popExitTransition = { + slideOutVertically(tween(TRANSITION_MS)) { it } + } + ) { + DeliveryScreen( + orderManager = orderManager, + onClose = { + orderManager.clearOrder() + navController.popBackStack(ROUTE_WELCOME, inclusive = false) + } + ) + } } } } } } - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - MilliwaysTheme { - Greeting("Android") - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/mobilenext/Milliways/model/MenuItem.kt b/android/app/src/main/java/com/mobilenext/Milliways/model/MenuItem.kt new file mode 100644 index 0000000..ae1506b --- /dev/null +++ b/android/app/src/main/java/com/mobilenext/Milliways/model/MenuItem.kt @@ -0,0 +1,16 @@ +package com.mobilenext.Milliways.model + +import androidx.annotation.DrawableRes +import androidx.compose.ui.graphics.Color +import java.util.UUID + +data class MenuItem( + val id: String = UUID.randomUUID().toString(), + val name: String, + val description: String, + val price: Double, + val color: Color, + @DrawableRes val imageRes: Int? = null +) + +fun formatPrice(value: Double): String = "β‚­%.2f".format(value) diff --git a/android/app/src/main/java/com/mobilenext/Milliways/model/OrderManager.kt b/android/app/src/main/java/com/mobilenext/Milliways/model/OrderManager.kt new file mode 100644 index 0000000..1c25fa1 --- /dev/null +++ b/android/app/src/main/java/com/mobilenext/Milliways/model/OrderManager.kt @@ -0,0 +1,52 @@ +package com.mobilenext.Milliways.model + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.mutableStateListOf +import java.util.UUID + +data class OrderItem( + val menuItem: MenuItem, + var quantity: Int, + val id: String = UUID.randomUUID().toString() +) { + val totalPrice: Double get() = quantity * menuItem.price +} + +class OrderManager { + val items: SnapshotStateList = mutableStateListOf() + var couponDiscount by mutableStateOf(0.0) + private set + var appliedCouponCode: String? by mutableStateOf(null) + private set + + val totalPrice: Double get() = items.sumOf { it.totalPrice } + + // Coupon discount is frozen when applied β€” does not update if cart changes + val finalTotal: Double get() = totalPrice - couponDiscount + + val totalQuantity: Int get() = items.sumOf { it.quantity } + + fun addItem(item: MenuItem, quantity: Int) { + items.add(OrderItem(item, quantity)) + } + + fun removeItem(id: String) { + items.removeAll { it.id == id } + } + + fun applyCoupon(code: String): Boolean { + if (!code.equals("MARVIN", ignoreCase = true)) return false + couponDiscount = 20.0 + appliedCouponCode = "MARVIN" + return true + } + + fun clearOrder() { + items.clear() + couponDiscount = 0.0 + appliedCouponCode = null + } +} diff --git a/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/AccountDialog.kt b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/AccountDialog.kt new file mode 100644 index 0000000..2a51fa5 --- /dev/null +++ b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/AccountDialog.kt @@ -0,0 +1,145 @@ +package com.mobilenext.Milliways.ui.screens + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +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.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.mobilenext.Milliways.R +import com.mobilenext.Milliways.model.formatPrice +import com.mobilenext.Milliways.ui.theme.MilliwaysOrange + +private data class PastOrder(val date: String, val total: Double) + +private val pastOrders = listOf( + PastOrder("Feb 17, 2065", 78.50), + PastOrder("Jun 3, 2065", 45.75), + PastOrder("Oct 12, 2065", 112.00) +) + +@Composable +fun AccountDialog(onDismiss: () -> Unit) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().statusBarsPadding().padding(4.dp) + ) { + Text( + "My Account", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.weight(1f).padding(start = 44.dp), + textAlign = androidx.compose.ui.text.style.TextAlign.Center + ) + IconButton(onClick = onDismiss) { + Icon(Icons.Filled.Close, contentDescription = "Close") + } + } + + LazyColumn(modifier = Modifier.fillMaxSize()) { + item { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(16.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Hi Glorpax!", fontSize = 26.sp, fontWeight = FontWeight.Bold) + Text("Pro Cosmic Foodie 🌌", color = MilliwaysOrange, style = MaterialTheme.typography.bodyMedium) + } + Image( + painter = painterResource(R.drawable.avatar), + contentDescription = "Avatar", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(70.dp) + .clip(CircleShape) + .border(2.dp, MilliwaysOrange, CircleShape) + ) + } + HorizontalDivider() + } + item { + Row( + modifier = Modifier.fillMaxWidth().padding(16.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + AccountStat("3", "Orders") + AccountStat("β‚­431.93", "Total Spent") + AccountStat("19", "Light-years") + } + HorizontalDivider() + } + item { + Text( + "Past Orders", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp) + ) + } + items(pastOrders) { order -> + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text(order.date, fontWeight = FontWeight.SemiBold) + Text( + "Delivered across the galaxy", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text(formatPrice(order.total), color = MilliwaysOrange, fontWeight = FontWeight.SemiBold) + } + } + } + } + } +} + +@Composable +private fun AccountStat(value: String, label: String) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(value, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Text(label, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } +} diff --git a/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/DeliveryScreen.kt b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/DeliveryScreen.kt new file mode 100644 index 0000000..8fcdde5 --- /dev/null +++ b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/DeliveryScreen.kt @@ -0,0 +1,101 @@ +package com.mobilenext.Milliways.ui.screens + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableDoubleStateOf +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.res.painterResource +import androidx.compose.ui.text.font.FontFamily +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.mobilenext.Milliways.R +import com.mobilenext.Milliways.model.OrderManager +import kotlinx.coroutines.delay +import kotlin.random.Random + +@Composable +fun DeliveryScreen( + orderManager: OrderManager, + onClose: () -> Unit +) { + // Ordering with an empty cart intentionally crashes here (index 0 of an empty list), + // mirroring the iOS app's out-of-bounds crash on the same flow. + val firstItemName = orderManager.items[0].menuItem.name + + var minutesRemaining by remember { mutableDoubleStateOf(Random.nextDouble(2_000_000.0, 3_000_000.0)) } + + LaunchedEffect(Unit) { + while (true) { + delay(1000L / 30L) + if (minutesRemaining > 0) { + minutesRemaining -= Random.nextDouble(0.01, 0.05) + } + } + } + + Box(modifier = Modifier.fillMaxSize()) { + Image( + painter = painterResource(R.drawable.nebula_map), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + + Column(modifier = Modifier.fillMaxSize()) { + Text( + "Your $firstItemName is on its way!", + color = Color.White, + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth().padding(top = 120.dp, start = 16.dp, end = 16.dp) + ) + } + + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 60.dp) + .background(Color.Black.copy(alpha = 0.5f), RoundedCornerShape(12.dp)) + .padding(16.dp) + ) { + Text( + "%.8f minutes for delivery".format(minutesRemaining), + color = Color.White, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace + ) + } + + IconButton( + onClick = onClose, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 50.dp, end = 20.dp) + ) { + Icon(Icons.Filled.Close, contentDescription = "Close", tint = Color.White) + } + } +} diff --git a/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/MenuData.kt b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/MenuData.kt new file mode 100644 index 0000000..1469c91 --- /dev/null +++ b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/MenuData.kt @@ -0,0 +1,28 @@ +package com.mobilenext.Milliways.ui.screens + +import androidx.compose.ui.graphics.Color +import com.mobilenext.Milliways.R +import com.mobilenext.Milliways.model.MenuItem + +data class MenuSection(val title: String, val items: List) + +val mainDishes = listOf( + MenuItem(name = "Ameglian Major Cow", description = "The finest cut from a cow that wants to be eaten", price = 35.00, color = Color(0xFF8B5A2B), imageRes = R.drawable.steak), + MenuItem(name = "Green Salad", description = "Fresh greens from the hydroponic gardens of Alpha Centauri", price = 22.00, color = Color(0xFF34C759), imageRes = R.drawable.green_salad), + MenuItem(name = "Soup of the Day", description = "Today's special soup, ingredients vary by solar system", price = 28.00, color = Color(0xFFFF9500), imageRes = R.drawable.soup), + MenuItem(name = "Quantum Shrimp Cascade", description = "A bowl of shrimp that exist in multiple flavor states until you take the first bite", price = 38.00, color = Color(0xFFFF2D92), imageRes = R.drawable.shrimp), + MenuItem(name = "Krikkit Fried Logic", description = "An impossible dish that paradoxically tastes like everything you've ever eaten and nothing at all", price = 40.00, color = Color(0xFF32ADE6), imageRes = R.drawable.fried_logic) +) + +val drinks = listOf( + MenuItem(name = "Pan Galactic Gargle Blaster", description = "Like having your brains smashed out by a slice of lemon wrapped around a large gold brick", price = 5.50, color = Color(0xFFFFCC00), imageRes = R.drawable.pan_galactic_gargle_blaster), + MenuItem(name = "Water", description = "Pure H2O from Earth's finest springs", price = 3.00, color = Color(0xFF007AFF), imageRes = R.drawable.water), + MenuItem(name = "Coffee", description = "Hot caffeinated beverage to keep you awake through the apocalypse", price = 4.50, color = Color(0xFF8B5A2B), imageRes = R.drawable.coffee), + MenuItem(name = "Infinite Improbability Float", description = "Odds of getting the same flavor twice are approximately 1 in 10⁸⁰⁰⁰⁰⁰", price = 6.00, color = Color(0xFFAF52DE), imageRes = R.drawable.infinite_improbability_float), + MenuItem(name = "Dark Matter Martini", description = "So dense it distorts the glass around it; one sip and your hangover develops before you finish drinking", price = 5.75, color = Color(0xFF000000), imageRes = R.drawable.dark_matter_martini) +) + +val menuSections = listOf( + MenuSection("MAIN DISHES", mainDishes), + MenuSection("DRINKS", drinks) +) diff --git a/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/MenuItemCard.kt b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/MenuItemCard.kt new file mode 100644 index 0000000..10dbca7 --- /dev/null +++ b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/MenuItemCard.kt @@ -0,0 +1,74 @@ +package com.mobilenext.Milliways.ui.screens + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.mobilenext.Milliways.model.MenuItem +import com.mobilenext.Milliways.model.formatPrice +import com.mobilenext.Milliways.ui.theme.MilliwaysBlue + +@Composable +fun MenuItemCard(item: MenuItem, onClick: () -> Unit) { + Column(modifier = Modifier.clickable(onClick = onClick)) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text(item.name, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) + Text( + item.description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 4.dp) + ) + Text( + formatPrice(item.price), + style = MaterialTheme.typography.bodyMedium, + color = MilliwaysBlue, + modifier = Modifier.padding(top = 4.dp) + ) + } + + if (item.imageRes != null) { + Image( + painter = painterResource(item.imageRes), + contentDescription = item.name, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(60.dp) + .clip(RoundedCornerShape(8.dp)) + ) + } else { + Column( + modifier = Modifier + .size(60.dp) + .clip(RoundedCornerShape(8.dp)) + .background(item.color) + ) {} + } + } + HorizontalDivider(color = Color.Gray.copy(alpha = 0.3f)) + } +} diff --git a/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/MenuItemDetailDialog.kt b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/MenuItemDetailDialog.kt new file mode 100644 index 0000000..3fb5d48 --- /dev/null +++ b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/MenuItemDetailDialog.kt @@ -0,0 +1,156 @@ +package com.mobilenext.Milliways.ui.screens + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +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.layout.statusBarsPadding +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.Close +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.mobilenext.Milliways.model.MenuItem +import com.mobilenext.Milliways.model.formatPrice +import com.mobilenext.Milliways.ui.theme.MilliwaysGray6 +import com.mobilenext.Milliways.ui.theme.MilliwaysOrange + +@Composable +fun MenuItemDetailDialog( + item: MenuItem, + onDismiss: () -> Unit, + onAddToOrder: (quantity: Int) -> Unit +) { + var quantity by remember { mutableIntStateOf(1) } + + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + Box(modifier = Modifier.fillMaxWidth().height(320.dp)) { + if (item.imageRes != null) { + Image( + painter = painterResource(item.imageRes), + contentDescription = item.name, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + } else { + Box(modifier = Modifier.fillMaxSize().background(item.color)) + } + IconButton( + onClick = onDismiss, + modifier = Modifier.align(Alignment.TopEnd).statusBarsPadding().padding(12.dp) + ) { + Icon( + Icons.Filled.Close, + contentDescription = "Close", + tint = Color.White, + modifier = Modifier + .size(30.dp) + .background(Color.Black.copy(alpha = 0.3f), CircleShape) + .padding(4.dp) + ) + } + } + + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()) + .padding(20.dp) + ) { + Text(item.name, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(8.dp)) + Text( + formatPrice(item.price), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + color = MilliwaysOrange + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + item.description, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .background(MilliwaysGray6, RoundedCornerShape(10.dp)) + ) { + IconButton(onClick = { if (quantity > 1) quantity-- }) { + Text( + "βˆ’", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = if (quantity > 1) MaterialTheme.colorScheme.onSurface else Color.Gray + ) + } + Text( + "$quantity", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.width(36.dp), + textAlign = androidx.compose.ui.text.style.TextAlign.Center + ) + IconButton(onClick = { quantity++ }) { + Text("+", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + } + } + + Spacer(modifier = Modifier.width(12.dp)) + + Button( + onClick = { onAddToOrder(quantity) }, + colors = ButtonDefaults.buttonColors(containerColor = MilliwaysOrange), + shape = RoundedCornerShape(12.dp), + modifier = Modifier.weight(1f) + ) { + Text("Add to Order", color = Color.White, fontWeight = FontWeight.Bold) + } + } + } + } +} diff --git a/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/MenuScreen.kt b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/MenuScreen.kt new file mode 100644 index 0000000..6e85e0a --- /dev/null +++ b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/MenuScreen.kt @@ -0,0 +1,155 @@ +package com.mobilenext.Milliways.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +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.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.ShoppingCart +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +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.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.mobilenext.Milliways.model.MenuItem +import com.mobilenext.Milliways.model.OrderManager +import com.mobilenext.Milliways.model.formatPrice +import com.mobilenext.Milliways.ui.theme.MilliwaysBlue +import com.mobilenext.Milliways.ui.theme.MilliwaysOrange +import com.mobilenext.Milliways.ui.theme.MilliwaysRed + +@Composable +fun MenuScreen( + orderManager: OrderManager, + onBack: () -> Unit, + onViewOrder: () -> Unit +) { + var selectedItem by remember { mutableStateOf(null) } + + Box(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.fillMaxSize()) { + // Top bar + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .statusBarsPadding() + .padding(horizontal = 4.dp, vertical = 4.dp) + ) { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", tint = MilliwaysBlue) + } + Text( + "Milliways", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.weight(1f), + textAlign = androidx.compose.ui.text.style.TextAlign.Center + ) + Box { + IconButton(onClick = onViewOrder) { + Icon(Icons.Filled.ShoppingCart, contentDescription = "Cart", tint = MilliwaysBlue) + } + if (orderManager.totalQuantity > 0) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 4.dp, end = 4.dp) + .size(16.dp) + .background(MilliwaysRed, CircleShape), + contentAlignment = Alignment.Center + ) { + Text( + "${orderManager.totalQuantity}", + color = Color.White, + fontSize = 10.sp, + fontWeight = FontWeight.Bold + ) + } + } + } + } + + LazyColumn( + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(vertical = 12.dp) + ) { + menuSections.forEach { section -> + item { + Text( + section.title, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + items(section.items) { menuItem -> + Box(modifier = Modifier.padding(horizontal = 16.dp)) { + MenuItemCard(item = menuItem, onClick = { selectedItem = menuItem }) + } + } + } + item { + Text( + "* Shipping beyond 5 light-years distance might cost extra", + style = MaterialTheme.typography.bodySmall, + fontStyle = FontStyle.Italic, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 20.dp, bottom = 40.dp) + ) + } + } + + if (orderManager.items.isNotEmpty()) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .background(MilliwaysOrange, RoundedCornerShape(12.dp)) + .clickable(onClick = onViewOrder) + .padding(16.dp) + ) { + Text("View Order", color = Color.White, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f)) + Text("${orderManager.totalQuantity} items", color = Color.White, modifier = Modifier.padding(end = 8.dp)) + Text(formatPrice(orderManager.totalPrice), color = Color.White, fontWeight = FontWeight.Bold) + } + } + } + } + + selectedItem?.let { item -> + MenuItemDetailDialog( + item = item, + onDismiss = { selectedItem = null }, + onAddToOrder = { quantity -> + orderManager.addItem(item, quantity) + selectedItem = null + } + ) + } +} diff --git a/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/OrderScreen.kt b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/OrderScreen.kt new file mode 100644 index 0000000..0b0c93c --- /dev/null +++ b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/OrderScreen.kt @@ -0,0 +1,253 @@ +package com.mobilenext.Milliways.ui.screens + +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.ShoppingCart +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxValue +import androidx.compose.material3.Text +import androidx.compose.material3.rememberSwipeToDismissBoxState +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.mobilenext.Milliways.model.OrderManager +import com.mobilenext.Milliways.model.formatPrice +import com.mobilenext.Milliways.ui.theme.MilliwaysBlue +import com.mobilenext.Milliways.ui.theme.MilliwaysGreen +import com.mobilenext.Milliways.ui.theme.MilliwaysGroupedBackground +import com.mobilenext.Milliways.ui.theme.MilliwaysRed + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OrderScreen( + orderManager: OrderManager, + onBack: () -> Unit, + onPlaceOrder: () -> Unit +) { + var couponCode by remember { mutableStateOf("") } + var couponError by remember { mutableStateOf(null) } + + Column( + modifier = Modifier + .fillMaxSize() + .background(MilliwaysGroupedBackground) + .imePadding() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().statusBarsPadding().padding(4.dp) + ) { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", tint = MilliwaysBlue) + } + Text( + "Your Order", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.weight(1f), + textAlign = androidx.compose.ui.text.style.TextAlign.Center + ) + Box(modifier = Modifier.size(48.dp)) + } + + if (orderManager.items.isEmpty()) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.weight(1f).fillMaxWidth() + ) { + Icon( + Icons.Filled.ShoppingCart, + contentDescription = null, + tint = Color.Gray, + modifier = Modifier.size(80.dp) + ) + Text( + "Your order is empty", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 20.dp) + ) + } + + // Deliberately left enabled with an empty cart β€” placing an order here + // crashes on the delivery screen (see DeliveryScreen), mirroring the iOS app. + Button( + onClick = onPlaceOrder, + colors = ButtonDefaults.buttonColors(containerColor = MilliwaysGreen), + shape = RoundedCornerShape(12.dp), + modifier = Modifier.fillMaxWidth().padding(16.dp) + ) { + Text("Place Order", color = Color.White, fontWeight = FontWeight.Bold) + } + } else { + LazyColumn(modifier = Modifier.weight(1f)) { + items(orderManager.items, key = { it.id }) { orderItem -> + val dismissState = rememberSwipeToDismissBoxState( + confirmValueChange = { value -> + if (value == SwipeToDismissBoxValue.EndToStart) { + orderManager.removeItem(orderItem.id) + true + } else { + false + } + } + ) + SwipeToDismissBox( + state = dismissState, + enableDismissFromStartToEnd = false, + backgroundContent = { + Row( + modifier = Modifier + .fillMaxSize() + .background(MilliwaysRed) + .padding(horizontal = 20.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Filled.Delete, contentDescription = "Delete", tint = Color.White) + } + } + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + if (orderItem.menuItem.imageRes != null) { + Image( + painter = painterResource(orderItem.menuItem.imageRes), + contentDescription = orderItem.menuItem.name, + contentScale = ContentScale.Crop, + modifier = Modifier.size(50.dp).clip(RoundedCornerShape(8.dp)) + ) + } else { + Box(modifier = Modifier.size(50.dp).clip(RoundedCornerShape(8.dp)).background(orderItem.menuItem.color)) + } + Column(modifier = Modifier.weight(1f).padding(start = 12.dp)) { + Text(orderItem.menuItem.name, fontWeight = FontWeight.SemiBold) + Text( + "${orderItem.quantity} Γ— ${formatPrice(orderItem.menuItem.price)}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text(formatPrice(orderItem.totalPrice), fontWeight = FontWeight.SemiBold) + } + } + } + + if (orderManager.couponDiscount > 0) { + item { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Text( + "Coupon ${orderManager.appliedCouponCode ?: ""}", + color = MilliwaysGreen, + modifier = Modifier.weight(1f) + ) + Text("-${formatPrice(orderManager.couponDiscount)}", color = MilliwaysGreen) + } + } + } + + item { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp) + ) { + Text("Total", fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f)) + Text( + formatPrice(orderManager.finalTotal), + fontWeight = FontWeight.Bold, + color = if (orderManager.finalTotal < 0) MilliwaysRed else MaterialTheme.colorScheme.onSurface + ) + } + } + } + + if (orderManager.appliedCouponCode == null) { + Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + value = couponCode, + onValueChange = { couponCode = it.uppercase() }, + label = { Text("Coupon code") }, + singleLine = true, + modifier = Modifier.weight(1f) + ) + Button( + onClick = { + if (orderManager.applyCoupon(couponCode)) { + couponCode = "" + couponError = null + } else { + couponError = "Invalid coupon code" + } + }, + modifier = Modifier.padding(start = 8.dp) + ) { + Text("Apply") + } + } + couponError?.let { + Text(it, color = MilliwaysRed, style = MaterialTheme.typography.bodySmall) + } + } + } + + Button( + onClick = { + // Coupon discount is frozen at apply-time and can drive the total negative; + // placing an order in that state crashes, mirroring the iOS app's UInt trap. + check(orderManager.finalTotal >= 0.0) { + "Negative total: ${orderManager.finalTotal}" + } + val cents = (orderManager.finalTotal * 100).toUInt() + println("Processing payment of $cents cents") + onPlaceOrder() + }, + colors = ButtonDefaults.buttonColors(containerColor = MilliwaysGreen), + shape = RoundedCornerShape(12.dp), + modifier = Modifier.fillMaxWidth().padding(16.dp) + ) { + Text("Place Order", color = Color.White, fontWeight = FontWeight.Bold) + } + } + } +} diff --git a/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/WelcomeScreen.kt b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/WelcomeScreen.kt new file mode 100644 index 0000000..362b519 --- /dev/null +++ b/android/app/src/main/java/com/mobilenext/Milliways/ui/screens/WelcomeScreen.kt @@ -0,0 +1,121 @@ +package com.mobilenext.Milliways.ui.screens + +import androidx.compose.animation.core.FastOutSlowInEasing +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.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +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.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.mobilenext.Milliways.R +import com.mobilenext.Milliways.model.OrderManager +import com.mobilenext.Milliways.ui.theme.MilliwaysOrange + +@Composable +fun WelcomeScreen( + orderManager: OrderManager, + onNewOrder: () -> Unit +) { + var showAccount by remember { mutableStateOf(false) } + + Box(modifier = Modifier.fillMaxSize()) { + Image( + painter = painterResource(R.drawable.hero_background), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + + Column(modifier = Modifier.fillMaxSize()) { + Text( + text = "Welcome to Milliways", + color = Color.White, + fontSize = 28.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .fillMaxWidth() + .padding(top = 60.dp), + textAlign = androidx.compose.ui.text.style.TextAlign.Center + ) + + val infiniteTransition = rememberInfiniteTransition(label = "float") + val floatOffset by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 20f, + animationSpec = infiniteRepeatable( + animation = tween(2000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse + ), + label = "floatOffset" + ) + + Image( + painter = painterResource(R.drawable.hero_foreground), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 40.dp) + .offset(y = (floatOffset - 40).dp) + ) + + Button( + onClick = onNewOrder, + shape = CircleShape, + colors = ButtonDefaults.buttonColors(containerColor = MilliwaysOrange), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 60.dp) + .padding(bottom = 24.dp) + ) { + Text("New Order", color = Color.White, fontWeight = FontWeight.Bold) + } + } + + IconButton( + onClick = { showAccount = true }, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 48.dp, end = 12.dp) + ) { + Icon( + imageVector = Icons.Filled.AccountCircle, + contentDescription = "Account", + tint = Color.White, + modifier = Modifier.padding(4.dp) + ) + } + } + + if (showAccount) { + AccountDialog(onDismiss = { showAccount = false }) + } +} diff --git a/android/app/src/main/java/com/mobilenext/Milliways/ui/theme/Color.kt b/android/app/src/main/java/com/mobilenext/Milliways/ui/theme/Color.kt index f2f39a4..1e22160 100644 --- a/android/app/src/main/java/com/mobilenext/Milliways/ui/theme/Color.kt +++ b/android/app/src/main/java/com/mobilenext/Milliways/ui/theme/Color.kt @@ -8,4 +8,12 @@ val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +val Pink40 = Color(0xFF7D5260) + +// Milliways brand colors β€” mirrors the iOS app's inline SwiftUI system colors +val MilliwaysOrange = Color(0xFFFF9500) +val MilliwaysGreen = Color(0xFF34C759) +val MilliwaysRed = Color(0xFFFF3B30) +val MilliwaysBlue = Color(0xFF007AFF) +val MilliwaysGray6 = Color(0xFFE5E5EA) +val MilliwaysGroupedBackground = Color(0xFFF2F2F7) \ No newline at end of file diff --git a/android/app/src/main/java/com/mobilenext/Milliways/ui/theme/Theme.kt b/android/app/src/main/java/com/mobilenext/Milliways/ui/theme/Theme.kt index 19722ed..34ba320 100644 --- a/android/app/src/main/java/com/mobilenext/Milliways/ui/theme/Theme.kt +++ b/android/app/src/main/java/com/mobilenext/Milliways/ui/theme/Theme.kt @@ -9,35 +9,30 @@ 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 private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 + primary = MilliwaysBlue, + secondary = MilliwaysOrange, + tertiary = Pink80, + background = Color(0xFF000000), + surface = Color(0xFF1C1C1E) ) private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ + primary = MilliwaysBlue, + secondary = MilliwaysOrange, + tertiary = Pink40, + background = Color(0xFFFFFFFF), + surface = Color(0xFFFFFFFF) ) @Composable fun MilliwaysTheme( darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, + // The iOS app uses fixed brand colors rather than Material You, so dynamic color is off by default + dynamicColor: Boolean = false, content: @Composable () -> Unit ) { val colorScheme = when { diff --git a/android/app/src/main/res/drawable/avatar.jpg b/android/app/src/main/res/drawable/avatar.jpg new file mode 100644 index 0000000..97fb7d8 Binary files /dev/null and b/android/app/src/main/res/drawable/avatar.jpg differ diff --git a/android/app/src/main/res/drawable/coffee.jpg b/android/app/src/main/res/drawable/coffee.jpg new file mode 100644 index 0000000..4df51e7 Binary files /dev/null and b/android/app/src/main/res/drawable/coffee.jpg differ diff --git a/android/app/src/main/res/drawable/dark_matter_martini.jpg b/android/app/src/main/res/drawable/dark_matter_martini.jpg new file mode 100644 index 0000000..c5a9414 Binary files /dev/null and b/android/app/src/main/res/drawable/dark_matter_martini.jpg differ diff --git a/android/app/src/main/res/drawable/fried_logic.jpg b/android/app/src/main/res/drawable/fried_logic.jpg new file mode 100644 index 0000000..6c2d89d Binary files /dev/null and b/android/app/src/main/res/drawable/fried_logic.jpg differ diff --git a/android/app/src/main/res/drawable/green_salad.jpg b/android/app/src/main/res/drawable/green_salad.jpg new file mode 100644 index 0000000..b6b3eb8 Binary files /dev/null and b/android/app/src/main/res/drawable/green_salad.jpg differ diff --git a/android/app/src/main/res/drawable/hero_background.jpg b/android/app/src/main/res/drawable/hero_background.jpg new file mode 100644 index 0000000..4de1d1b Binary files /dev/null and b/android/app/src/main/res/drawable/hero_background.jpg differ diff --git a/android/app/src/main/res/drawable/hero_foreground.png b/android/app/src/main/res/drawable/hero_foreground.png new file mode 100644 index 0000000..3ab907c Binary files /dev/null and b/android/app/src/main/res/drawable/hero_foreground.png differ diff --git a/android/app/src/main/res/drawable/ic_launcher_background.png b/android/app/src/main/res/drawable/ic_launcher_background.png new file mode 100644 index 0000000..3e18c46 Binary files /dev/null and b/android/app/src/main/res/drawable/ic_launcher_background.png differ diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/android/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml index 2b068d1..76589c0 100644 --- a/android/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,30 +1,4 @@ - - - - - - - - - - - \ No newline at end of file + + + + diff --git a/android/app/src/main/res/drawable/infinite_improbability_float.jpg b/android/app/src/main/res/drawable/infinite_improbability_float.jpg new file mode 100644 index 0000000..37faf7c Binary files /dev/null and b/android/app/src/main/res/drawable/infinite_improbability_float.jpg differ diff --git a/android/app/src/main/res/drawable/nebula_map.jpg b/android/app/src/main/res/drawable/nebula_map.jpg new file mode 100644 index 0000000..044e629 Binary files /dev/null and b/android/app/src/main/res/drawable/nebula_map.jpg differ diff --git a/android/app/src/main/res/drawable/pan_galactic_gargle_blaster.jpg b/android/app/src/main/res/drawable/pan_galactic_gargle_blaster.jpg new file mode 100644 index 0000000..7396e96 Binary files /dev/null and b/android/app/src/main/res/drawable/pan_galactic_gargle_blaster.jpg differ diff --git a/android/app/src/main/res/drawable/shrimp.jpg b/android/app/src/main/res/drawable/shrimp.jpg new file mode 100644 index 0000000..eeb3a57 Binary files /dev/null and b/android/app/src/main/res/drawable/shrimp.jpg differ diff --git a/android/app/src/main/res/drawable/soup.jpg b/android/app/src/main/res/drawable/soup.jpg new file mode 100644 index 0000000..298ec19 Binary files /dev/null and b/android/app/src/main/res/drawable/soup.jpg differ diff --git a/android/app/src/main/res/drawable/steak.jpg b/android/app/src/main/res/drawable/steak.jpg new file mode 100644 index 0000000..099711f Binary files /dev/null and b/android/app/src/main/res/drawable/steak.jpg differ diff --git a/android/app/src/main/res/drawable/water.jpg b/android/app/src/main/res/drawable/water.jpg new file mode 100644 index 0000000..413c90b Binary files /dev/null and b/android/app/src/main/res/drawable/water.jpg differ diff --git a/android/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 74% rename from android/app/src/main/res/mipmap-anydpi/ic_launcher.xml rename to android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6f3b755..6b78462 100644 --- a/android/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,5 +2,4 @@ - - \ No newline at end of file + diff --git a/android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 74% rename from android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml rename to android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 6f3b755..6b78462 100644 --- a/android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,5 +2,4 @@ - - \ No newline at end of file + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..3e18c46 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..3e18c46 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..3e18c46 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..3e18c46 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..3e18c46 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..3e18c46 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..3e18c46 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..3e18c46 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..3e18c46 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..3e18c46 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 85f878f..8045111 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -8,6 +8,7 @@ espressoCore = "3.7.0" lifecycleRuntimeKtx = "2.10.0" activityCompose = "1.13.0" composeBom = "2024.09.00" +navigationCompose = "2.8.4" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -24,6 +25,8 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/tests/mobilewright.config.ts b/tests/mobilewright.config.ts index 061bd0e..8452342 100644 --- a/tests/mobilewright.config.ts +++ b/tests/mobilewright.config.ts @@ -32,14 +32,14 @@ const config: MobilewrightConfig = { name: "ios-simulator", use: { platform: "ios", - installApps: ["../build/Milliways-simulator.zip"], + installApps: ["../ios/build/Milliways-simulator.zip"], }, }, { name: "ios", use: { platform: "ios", - installApps: ["../build/Milliways-unsigned.ipa"], + installApps: ["../ios/build/Milliways-unsigned.ipa"], } }, ],