Artem Kobzar
7 months ago
committed by
GitHub
140 changed files with 12677 additions and 0 deletions
@ -0,0 +1,43 @@
|
||||
.gradle |
||||
build/ |
||||
!gradle/wrapper/gradle-wrapper.jar |
||||
!**/src/main/**/build/ |
||||
!**/src/test/**/build/ |
||||
|
||||
### IntelliJ IDEA ### |
||||
.idea/modules.xml |
||||
.idea/jarRepositories.xml |
||||
.idea/compiler.xml |
||||
.idea/libraries/ |
||||
*.iws |
||||
*.iml |
||||
*.ipr |
||||
out/ |
||||
!**/src/main/**/out/ |
||||
!**/src/test/**/out/ |
||||
|
||||
### Eclipse ### |
||||
.apt_generated |
||||
.classpath |
||||
.factorypath |
||||
.project |
||||
.settings |
||||
.springBeans |
||||
.sts4-cache |
||||
bin/ |
||||
!**/src/main/**/bin/ |
||||
!**/src/test/**/bin/ |
||||
|
||||
### NetBeans ### |
||||
/nbproject/private/ |
||||
/nbbuild/ |
||||
/dist/ |
||||
/nbdist/ |
||||
/.nb-gradle/ |
||||
|
||||
### VS Code ### |
||||
.vscode/ |
||||
|
||||
### Mac OS ### |
||||
.DS_Store |
||||
/local.properties |
@ -0,0 +1,122 @@
|
||||
# Kotlin/Wasm Jetsnack example |
||||
|
||||
This example showcases a web version of the [Jetsnack application](https://github.com/android/compose-samples/tree/main/Jetsnack) built with [Compose Multiplatform for web](#compose-multiplatform-for-web) and [Kotlin/Wasm](#kotlinwasm). |
||||
|
||||
Check it out: |
||||
|
||||
[![Static Badge](https://img.shields.io/badge/online%20demo%20%F0%9F%9A%80-6b57ff?style=for-the-badge)](https://zal.im/wasm/jetsnack). |
||||
|
||||
![](screenshots/jetsnack.png) |
||||
|
||||
> **Note:** |
||||
> To learn more about the Jetsnack application, visit the [Jetsnack README.md](https://github.com/android/compose-samples/tree/main/Jetsnack). |
||||
|
||||
## Kotlin/Wasm |
||||
|
||||
> **Note:** |
||||
> Kotlin/Wasm is an [Alpha](https://kotlinlang.org/docs/components-stability.html) feature. It may be changed at any time. You can use it in scenarios before production. |
||||
> We would appreciate your feedback in [YouTrack](https://youtrack.jetbrains.com/issue/KT-56492). |
||||
> |
||||
> [Join the Kotlin/Wasm community](https://slack-chats.kotlinlang.org/c/webassembly). |
||||
|
||||
Kotlin/Wasm is a new target that enables you to compile Kotlin code to [WebAssembly (Wasm)](https://webassembly.org/). |
||||
|
||||
By compiling Kotlin code to WebAssembly, you can run it on any WebAssembly-compatible environment that meets Kotlin's requirements, including web browsers. |
||||
|
||||
With Kotlin/Wasm, you gain the capability to develop high-performance web applications and serverless functions, opening up a wide range of potential projects. |
||||
|
||||
## Compose Multiplatform for web |
||||
|
||||
> **Note:** |
||||
> Web support is an [Alpha](https://kotlinlang.org/docs/components-stability.html) feature. It may be changed at any time. |
||||
> You can use it in scenarios before production. |
||||
> We would appreciate your feedback in [GitHub](https://github.com/JetBrains/compose-multiplatform/issues). |
||||
> |
||||
> [Join the compose-web community](https://slack-chats.kotlinlang.org/c/compose-web). |
||||
|
||||
Compose Multiplatform for web enables sharing your mobile or desktop UIs on the web. |
||||
|
||||
Compose Multiplatform for web is based on [Kotlin/Wasm](https://kotl.in/wasm), the newest target for Kotlin Multiplatform projects. |
||||
This enables running your code in the browser, leveraging WebAssembly's advantages like high and consistent application performance. |
||||
|
||||
Follow the instructions in the sections below to try out this Jetsnack application built with Compose Multiplatform for web and Kotlin/Wasm. |
||||
|
||||
## Set up the environment |
||||
|
||||
Before starting, ensure you have the necessary IDE and browser setup to run the application. |
||||
|
||||
### IDE |
||||
|
||||
We recommend using [IntelliJ IDEA 2023.1 or later](https://www.jetbrains.com/idea/) to work with the project. |
||||
It supports Kotlin/Wasm out of the box. |
||||
|
||||
### Browser (for Kotlin/Wasm target) |
||||
|
||||
To run Kotlin/Wasm applications in a browser, you need a browser supporting the [Wasm Garbage Collection (GC) feature](https://github.com/WebAssembly/gc): |
||||
|
||||
**Chrome and Chromium-based** |
||||
|
||||
* **For version 119 or later:** |
||||
|
||||
Works by default. |
||||
|
||||
**Firefox** |
||||
|
||||
* **For version 120 or later:** |
||||
|
||||
Works by default. |
||||
|
||||
**Safari/WebKit** |
||||
|
||||
Wasm GC support is currently under |
||||
[active development](https://bugs.webkit.org/show_bug.cgi?id=247394). |
||||
|
||||
> **Note:** |
||||
> For more information about the browser versions, see the [Troubleshooting documentation](https://kotl.in/wasm_help/). |
||||
|
||||
## Build and run |
||||
|
||||
To build and run the Jetsnack application with Compose Multiplatform for web and Kotlin/Wasm: |
||||
|
||||
1. In IntelliJ IDEA, open the repository. |
||||
2. Navigate to the `compose-jetsnack` project folder. |
||||
3. Run the application by typing one of the following Gradle commands in the terminal: |
||||
|
||||
* **Web version:** |
||||
|
||||
`./gradlew :web:wasmJsRun` |
||||
<br> <br> |
||||
|
||||
Once the application starts, open the following URL in your browser: |
||||
|
||||
`http://localhost:8080` |
||||
|
||||
> **Note:** |
||||
> The port number can vary. If the port 8080 is unavailable, you can find the corresponding port number printed in the console |
||||
> after building the application. |
||||
<br> <br> |
||||
|
||||
* **Desktop version:** |
||||
|
||||
`./gradlew :desktop:run` |
||||
<br> <br> |
||||
|
||||
* **Android application:** |
||||
|
||||
`./gradlew :android:installDebug` |
||||
* |
||||
* **iOS application:** |
||||
|
||||
To setup the environment, please consult these [instructions](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-setup.html). |
||||
|
||||
## Feedback and questions |
||||
|
||||
Give it a try and share your feedback or questions in our [#compose-web](https://slack-chats.kotlinlang.org/c/compose-web) Slack channel. |
||||
[Get a Slack invite](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up). |
||||
You can also share your comments with [@bashorov](https://twitter.com/bashorov) on X (Twitter). |
||||
|
||||
## Learn more |
||||
|
||||
* [Compose Multiplatform](https://github.com/JetBrains/compose-multiplatform/#compose-multiplatform) |
||||
* [Kotlin/Wasm](https://kotl.in/wasm/) |
||||
* [Other Kotlin/Wasm examples](https://github.com/Kotlin/kotlin-wasm-examples/tree/main) |
@ -0,0 +1,41 @@
|
||||
plugins { |
||||
id("org.jetbrains.compose") |
||||
id("com.android.application") |
||||
kotlin("android") |
||||
} |
||||
|
||||
group "com.example" |
||||
version "1.0-SNAPSHOT" |
||||
|
||||
repositories { |
||||
jcenter() |
||||
} |
||||
|
||||
dependencies { |
||||
implementation(project(":common")) |
||||
implementation("androidx.activity:activity-compose:1.5.0") |
||||
} |
||||
|
||||
android { |
||||
compileSdk = 34 |
||||
namespace = "com.example.android" |
||||
defaultConfig { |
||||
applicationId = "com.example.android" |
||||
minSdk = 24 |
||||
targetSdk = 33 |
||||
versionCode = 1 |
||||
versionName = "1.0-SNAPSHOT" |
||||
} |
||||
compileOptions { |
||||
sourceCompatibility = JavaVersion.VERSION_17 |
||||
targetCompatibility = JavaVersion.VERSION_17 |
||||
} |
||||
kotlin { |
||||
jvmToolchain(17) |
||||
} |
||||
buildTypes { |
||||
getByName("release") { |
||||
isMinifyEnabled = false |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<!--Load images from Unsplash--> |
||||
<uses-permission android:name="android.permission.INTERNET" /> |
||||
|
||||
<application |
||||
android:allowBackup="false" |
||||
android:supportsRtl="true" |
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"> |
||||
<activity android:name=".MainActivity" android:exported="true"> |
||||
<intent-filter> |
||||
<action android:name="android.intent.action.MAIN"/> |
||||
<category android:name="android.intent.category.LAUNCHER"/> |
||||
</intent-filter> |
||||
</activity> |
||||
</application> |
||||
</manifest> |
@ -0,0 +1,36 @@
|
||||
package com.example.android |
||||
|
||||
import android.os.Bundle |
||||
import androidx.activity.compose.setContent |
||||
import androidx.appcompat.app.AppCompatActivity |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.ui.text.font.Font |
||||
import androidx.compose.ui.text.font.FontFamily |
||||
import androidx.compose.ui.text.font.FontWeight |
||||
import com.example.jetsnack.R |
||||
import com.example.jetsnack.ui.JetsnackApp |
||||
import com.example.jetsnack.ui.theme.Karla |
||||
import com.example.jetsnack.ui.theme.Montserrat |
||||
|
||||
class MainActivity : AppCompatActivity() { |
||||
override fun onCreate(savedInstanceState: Bundle?) { |
||||
super.onCreate(savedInstanceState) |
||||
|
||||
Montserrat = FontFamily( |
||||
Font(R.font.montserrat_light, FontWeight.Light), |
||||
Font(R.font.montserrat_regular, FontWeight.Normal), |
||||
Font(R.font.montserrat_medium, FontWeight.Medium), |
||||
Font(R.font.montserrat_semibold, FontWeight.SemiBold) |
||||
) |
||||
Karla = FontFamily( |
||||
Font(R.font.karla_regular, FontWeight.Normal), |
||||
Font(R.font.karla_bold, FontWeight.Bold) |
||||
) |
||||
|
||||
setContent { |
||||
MaterialTheme { |
||||
JetsnackApp() |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,29 @@
|
||||
import org.jetbrains.compose.ComposeExtension |
||||
|
||||
group "com.example" |
||||
version "1.0-SNAPSHOT" |
||||
|
||||
allprojects { |
||||
repositories { |
||||
google() |
||||
mavenCentral() |
||||
} |
||||
|
||||
afterEvaluate { |
||||
extensions.findByType(ComposeExtension::class.java)?.apply { |
||||
val kotlinGeneration = project.property("kotlin.generation") |
||||
val composeCompilerVersion = project.property("compose.compiler.version.$kotlinGeneration") as String |
||||
kotlinCompilerPlugin.set(composeCompilerVersion) |
||||
val kotlinVersion = project.property("kotlin.version.$kotlinGeneration") as String |
||||
kotlinCompilerPluginArgs.add("suppressKotlinVersionCompatibilityCheck=$kotlinVersion") |
||||
} |
||||
} |
||||
} |
||||
|
||||
plugins { |
||||
kotlin("multiplatform") apply false |
||||
kotlin("android") apply false |
||||
id("com.android.application") apply false |
||||
id("com.android.library") apply false |
||||
id("org.jetbrains.compose") apply false |
||||
} |
@ -0,0 +1,110 @@
|
||||
import org.jetbrains.compose.compose |
||||
|
||||
plugins { |
||||
kotlin("multiplatform") |
||||
id("org.jetbrains.compose") |
||||
id("com.android.library") |
||||
} |
||||
|
||||
group = "com.example" |
||||
version = "1.0-SNAPSHOT" |
||||
|
||||
kotlin { |
||||
androidTarget() |
||||
wasmJs { browser() } |
||||
|
||||
jvm("desktop") { |
||||
compilations.all { |
||||
kotlinOptions.jvmTarget = "17" |
||||
} |
||||
} |
||||
|
||||
listOf( |
||||
iosX64(), |
||||
iosArm64(), |
||||
iosSimulatorArm64() |
||||
).forEach { iosTarget -> |
||||
iosTarget.binaries.framework { |
||||
baseName = "common" |
||||
isStatic = true |
||||
} |
||||
} |
||||
|
||||
applyDefaultHierarchyTemplate() |
||||
|
||||
sourceSets { |
||||
val commonMain by getting { |
||||
dependencies { |
||||
api(compose.runtime) |
||||
api(compose.foundation) |
||||
api(compose.material) |
||||
implementation(libs.kotlinx.coroutines) |
||||
} |
||||
} |
||||
val nonAndroidMain by creating { |
||||
dependsOn(commonMain) |
||||
} |
||||
val commonTest by getting { |
||||
dependencies { |
||||
implementation(kotlin("test")) |
||||
} |
||||
} |
||||
val androidMain by getting { |
||||
dependencies { |
||||
api("androidx.appcompat:appcompat:1.5.1") |
||||
api("androidx.core:core-ktx:1.9.0") |
||||
implementation(libs.coil.kt.compose) |
||||
implementation(libs.androidx.navigation.compose) |
||||
implementation(libs.androidx.compose.ui.util) |
||||
implementation(libs.androidx.lifecycle.viewModelCompose) |
||||
implementation(libs.androidx.constraintlayout.compose) |
||||
implementation(libs.androidx.core.ktx) |
||||
implementation(libs.androidx.activity.compose) |
||||
implementation(libs.androidx.lifecycle.viewModelCompose) |
||||
implementation(libs.androidx.lifecycle.runtime.compose) |
||||
} |
||||
} |
||||
|
||||
val androidUnitTest by getting { |
||||
dependencies { |
||||
implementation("junit:junit:4.13.2") |
||||
} |
||||
} |
||||
val desktopMain by getting { |
||||
dependsOn(nonAndroidMain) |
||||
dependencies { |
||||
api(compose.preview) |
||||
} |
||||
} |
||||
val desktopTest by getting |
||||
|
||||
val wasmJsMain by getting { |
||||
dependencies { |
||||
implementation(kotlin("stdlib")) |
||||
} |
||||
dependsOn(nonAndroidMain) |
||||
} |
||||
|
||||
val iosMain by getting { |
||||
dependsOn(nonAndroidMain) |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
android { |
||||
compileSdk = 34 |
||||
namespace = "com.example.jetsnack" |
||||
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") |
||||
defaultConfig { |
||||
minSdk = 24 |
||||
targetSdk = 33 |
||||
} |
||||
compileOptions { |
||||
sourceCompatibility = JavaVersion.VERSION_17 |
||||
targetCompatibility = JavaVersion.VERSION_17 |
||||
} |
||||
kotlin { |
||||
jvmToolchain(17) |
||||
} |
||||
} |
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"/> |
@ -0,0 +1,13 @@
|
||||
package com.example.jetsnack |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.graphics.painter.Painter |
||||
|
||||
@Composable |
||||
actual fun painterResource(id: Int): Painter { |
||||
return androidx.compose.ui.res.painterResource(id) |
||||
} |
||||
|
||||
actual val MppR.drawable.empty_state_search: Int |
||||
get() = R.drawable.empty_state_search |
||||
|
@ -0,0 +1,7 @@
|
||||
package com.example.jetsnack.model |
||||
|
||||
import java.util.* |
||||
|
||||
actual fun createRandomUUID(): Long { |
||||
return UUID.randomUUID().mostSignificantBits |
||||
} |
@ -0,0 +1,114 @@
|
||||
@file:Suppress("PrivatePropertyName") |
||||
|
||||
package com.example.jetsnack |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
|
||||
@Composable |
||||
actual fun stringResource(id: Int): String { |
||||
return androidx.compose.ui.res.stringResource(id) |
||||
} |
||||
|
||||
@Composable |
||||
actual fun stringResource(id: Int, part: String): String { |
||||
return androidx.compose.ui.res.stringResource(id, part) |
||||
} |
||||
|
||||
@Composable |
||||
actual fun stringResource(id: Int, count: Int): String { |
||||
return androidx.compose.ui.res.stringResource(id, count) |
||||
} |
||||
|
||||
|
||||
// Filters |
||||
actual val MppR.string.label_filters: Int get() = R.string.label_filters |
||||
|
||||
// Qty |
||||
actual val MppR.string.quantity: Int get() = R.string.quantity |
||||
|
||||
actual val MppR.string.label_decrease: Int get() = R.string.label_decrease |
||||
|
||||
actual val MppR.string.label_increase: Int get() = R.string.label_increase |
||||
|
||||
|
||||
// Snack detail |
||||
actual val MppR.string.label_back: Int get() = R.string.label_back |
||||
|
||||
actual val MppR.string.detail_header: Int get() = R.string.detail_header |
||||
|
||||
actual val MppR.string.detail_placeholder: Int get() = R.string.detail_placeholder |
||||
|
||||
actual val MppR.string.see_more: Int get() = R.string.see_more |
||||
|
||||
actual val MppR.string.see_less: Int get() = R.string.see_less |
||||
|
||||
actual val MppR.string.ingredients: Int get() = R.string.ingredients |
||||
|
||||
actual val MppR.string.ingredients_list: Int get() = R.string.ingredients_list |
||||
|
||||
actual val MppR.string.add_to_cart: Int get() = R.string.add_to_cart |
||||
|
||||
// Home |
||||
actual val MppR.string.label_select_delivery: Int get() = R.string.label_select_delivery |
||||
|
||||
|
||||
// Filter |
||||
actual val MppR.string.max_calories: Int get() = R.string.max_calories |
||||
|
||||
actual val MppR.string.per_serving: Int get() = R.string.per_serving |
||||
|
||||
actual val MppR.string.sort: Int get() = R.string.sort |
||||
|
||||
actual val MppR.string.lifestyle: Int get() = R.string.lifestyle |
||||
|
||||
actual val MppR.string.category: Int get() = R.string.category |
||||
|
||||
actual val MppR.string.price: Int get() = R.string.price |
||||
|
||||
actual val MppR.string.reset: Int get() = R.string.reset |
||||
|
||||
actual val MppR.string.close: Int get() = R.string.close |
||||
|
||||
// Profile |
||||
|
||||
actual val MppR.string.work_in_progress: Int get() = R.string.work_in_progress |
||||
|
||||
actual val MppR.string.grab_beverage: Int get() = R.string.grab_beverage |
||||
|
||||
// Home |
||||
actual val MppR.string.home_feed: Int get() = R.string.home_feed |
||||
|
||||
actual val MppR.string.home_search: Int get() = R.string.home_search |
||||
|
||||
actual val MppR.string.home_cart: Int get() = R.string.home_cart |
||||
|
||||
actual val MppR.string.home_profile: Int get() = R.string.home_profile |
||||
|
||||
|
||||
// Search |
||||
actual val MppR.string.search_no_matches: Int get() = R.string.search_no_matches |
||||
|
||||
actual val MppR.string.search_no_matches_retry: Int get() = R.string.search_no_matches_retry |
||||
|
||||
actual val MppR.string.label_add: Int get() = R.string.label_add |
||||
|
||||
actual val MppR.string.search_count: Int get() = R.string.search_count |
||||
|
||||
actual val MppR.string.label_search: Int get() = R.string.label_search |
||||
|
||||
actual val MppR.string.search_jetsnack: Int get() = R.string.search_jetsnack |
||||
|
||||
actual val MppR.string.cart_increase_error: Int get() = R.string.cart_increase_error |
||||
actual val MppR.string.cart_decrease_error: Int get() = R.string.cart_decrease_error |
||||
|
||||
|
||||
// Cart |
||||
actual val MppR.plurals.cart_order_count: Int get() = R.plurals.cart_order_count |
||||
actual val MppR.string.cart_order_header: Int get() = R.string.cart_order_header |
||||
actual val MppR.string.remove_item: Int get() = R.string.remove_item |
||||
actual val MppR.string.cart_summary_header: Int get() = R.string.cart_summary_header |
||||
actual val MppR.string.cart_subtotal_label: Int get() = R.string.cart_subtotal_label |
||||
actual val MppR.string.cart_shipping_label: Int get() = R.string.cart_shipping_label |
||||
actual val MppR.string.cart_total_label: Int get() = R.string.cart_total_label |
||||
actual val MppR.string.cart_checkout: Int get() = R.string.cart_checkout |
||||
actual val MppR.string.label_remove: Int get() = R.string.label_remove |
@ -0,0 +1,74 @@
|
||||
package com.example.jetsnack.ui |
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.navigation.NavBackStackEntry |
||||
import androidx.navigation.NavGraphBuilder |
||||
import androidx.navigation.NavType |
||||
import androidx.navigation.compose.NavHost |
||||
import androidx.navigation.compose.composable |
||||
import androidx.navigation.compose.navigation |
||||
import androidx.navigation.navArgument |
||||
import com.example.jetsnack.ui.home.Feed |
||||
import com.example.jetsnack.ui.home.HomeSections |
||||
import com.example.jetsnack.ui.home.Profile |
||||
import com.example.jetsnack.ui.home.cart.Cart |
||||
import com.example.jetsnack.ui.home.search.Search |
||||
import com.example.jetsnack.ui.snackdetail.SnackDetail |
||||
|
||||
@Composable |
||||
actual fun JetsnackScaffoldContent( |
||||
innerPaddingModifier: PaddingValues, |
||||
appState: MppJetsnackAppState |
||||
) { |
||||
NavHost( |
||||
navController = appState.navController, |
||||
startDestination = MainDestinations.HOME_ROUTE, |
||||
modifier = Modifier.padding(innerPaddingModifier) |
||||
) { |
||||
jetsnackNavGraph( |
||||
onSnackSelected = appState::navigateToSnackDetail, |
||||
upPress = appState::upPress |
||||
) |
||||
} |
||||
} |
||||
|
||||
private fun NavGraphBuilder.jetsnackNavGraph( |
||||
onSnackSelected: (Long, NavBackStackEntry) -> Unit, |
||||
upPress: () -> Unit, |
||||
) { |
||||
navigation( |
||||
route = MainDestinations.HOME_ROUTE, |
||||
startDestination = HomeSections.FEED.route |
||||
) { |
||||
addHomeGraph(onSnackSelected) |
||||
} |
||||
composable( |
||||
"${MainDestinations.SNACK_DETAIL_ROUTE}/{${MainDestinations.SNACK_ID_KEY}}", |
||||
arguments = listOf(navArgument(MainDestinations.SNACK_ID_KEY) { type = NavType.LongType }) |
||||
) { backStackEntry -> |
||||
val arguments = requireNotNull(backStackEntry.arguments) |
||||
val snackId = arguments.getLong(MainDestinations.SNACK_ID_KEY) |
||||
SnackDetail(snackId, upPress, onSnackClick = { onSnackSelected(snackId, backStackEntry) }) |
||||
} |
||||
} |
||||
|
||||
fun NavGraphBuilder.addHomeGraph( |
||||
onSnackSelected: (Long, NavBackStackEntry) -> Unit, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
composable(HomeSections.FEED.route) { from -> |
||||
Feed(onSnackClick = { id -> onSnackSelected(id, from) }, modifier) |
||||
} |
||||
composable(HomeSections.SEARCH.route) { from -> |
||||
Search(onSnackClick = { id -> onSnackSelected(id, from) }, modifier) |
||||
} |
||||
composable(HomeSections.CART.route) { from -> |
||||
Cart(onSnackClick = { id -> onSnackSelected(id, from) }, modifier) |
||||
} |
||||
composable(HomeSections.PROFILE.route) { |
||||
Profile(modifier) |
||||
} |
||||
} |
@ -0,0 +1,123 @@
|
||||
package com.example.jetsnack.ui |
||||
|
||||
import android.content.res.Resources |
||||
import androidx.compose.material.ScaffoldState |
||||
import androidx.compose.material.rememberScaffoldState |
||||
import androidx.compose.runtime.* |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.ui.platform.LocalConfiguration |
||||
import androidx.compose.ui.platform.LocalContext |
||||
import androidx.lifecycle.Lifecycle |
||||
import androidx.navigation.* |
||||
import androidx.navigation.compose.currentBackStackEntryAsState |
||||
import androidx.navigation.compose.rememberNavController |
||||
import com.example.jetsnack.model.SnackbarManager |
||||
import com.example.jetsnack.ui.home.HomeSections |
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.launch |
||||
|
||||
@Stable |
||||
actual class MppJetsnackAppState( |
||||
actual val scaffoldState: ScaffoldState, |
||||
actual val snackbarManager: SnackbarManager, |
||||
actual val coroutineScope: CoroutineScope, |
||||
val navController: NavHostController, |
||||
val resources: Resources |
||||
) { |
||||
|
||||
init { |
||||
coroutineScope.launch { |
||||
snackbarManager.messages.collect { currentMessages -> |
||||
if (currentMessages.isNotEmpty()) { |
||||
val message = currentMessages[0] |
||||
val text = resources.getText(message.message).toString() |
||||
|
||||
// Display the snackbar on the screen. `showSnackbar` is a function |
||||
// that suspends until the snackbar disappears from the screen |
||||
scaffoldState.snackbarHostState.showSnackbar(text) |
||||
// Once the snackbar is gone or dismissed, notify the SnackbarManager |
||||
snackbarManager.setMessageShown(message.id) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private val bottomBarRoutes = bottomBarTabs.map { it.route } |
||||
|
||||
actual val bottomBarTabs: Array<HomeSections> |
||||
get() = HomeSections.values() |
||||
actual val currentRoute: String? |
||||
get() = navController.currentDestination?.route |
||||
|
||||
|
||||
@Composable |
||||
actual fun shouldShowBottomBar(): Boolean { |
||||
return navController |
||||
.currentBackStackEntryAsState().value?.destination?.route in bottomBarRoutes |
||||
} |
||||
|
||||
actual fun navigateToBottomBarRoute(route: String) { |
||||
if (route != currentRoute) { |
||||
navController.navigate(route) { |
||||
launchSingleTop = true |
||||
restoreState = true |
||||
// Pop up backstack to the first destination and save state. This makes going back |
||||
// to the start destination when pressing back in any other bottom tab. |
||||
popUpTo(findStartDestination(navController.graph).id) { |
||||
saveState = true |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
fun navigateToSnackDetail(snackId: Long, from: NavBackStackEntry) { |
||||
// In order to discard duplicated navigation events, we check the Lifecycle |
||||
if (from.lifecycleIsResumed()) { |
||||
navController.navigate("${MainDestinations.SNACK_DETAIL_ROUTE}/$snackId") |
||||
} |
||||
} |
||||
|
||||
fun upPress() { |
||||
navController.navigateUp() |
||||
} |
||||
} |
||||
|
||||
@Suppress("UsePropertyAccessSyntax") |
||||
private fun NavBackStackEntry.lifecycleIsResumed() = |
||||
this.getLifecycle().currentState == Lifecycle.State.RESUMED |
||||
|
||||
private val NavGraph.startDestination: NavDestination? |
||||
get() = findNode(startDestinationId) |
||||
|
||||
/** |
||||
* Copied from similar function in NavigationUI.kt |
||||
* |
||||
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt |
||||
*/ |
||||
private tailrec fun findStartDestination(graph: NavDestination): NavDestination { |
||||
return if (graph is NavGraph) findStartDestination(graph.startDestination!!) else graph |
||||
} |
||||
|
||||
@Composable |
||||
actual fun rememberMppJetsnackAppState(): MppJetsnackAppState { |
||||
val scaffoldState = rememberScaffoldState() |
||||
val navController = rememberNavController() |
||||
val resources = resources() |
||||
val snackbarManager = SnackbarManager |
||||
val coroutineScope = rememberCoroutineScope() |
||||
|
||||
return remember(scaffoldState, navController, snackbarManager, resources, coroutineScope) { |
||||
MppJetsnackAppState(scaffoldState, snackbarManager, coroutineScope, navController, resources) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* A composable function that returns the [Resources]. It will be recomposed when `Configuration` |
||||
* gets updated. |
||||
*/ |
||||
@Composable |
||||
@ReadOnlyComposable |
||||
private fun resources(): Resources { |
||||
LocalConfiguration.current |
||||
return LocalContext.current.resources |
||||
} |
@ -0,0 +1,25 @@
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.layout.ContentScale |
||||
import androidx.compose.ui.platform.LocalContext |
||||
import androidx.compose.ui.res.painterResource |
||||
import coil.compose.AsyncImage |
||||
import coil.request.ImageRequest |
||||
import com.example.jetsnack.R |
||||
|
||||
@Composable |
||||
actual fun SnackAsyncImage(imageUrl: String, contentDescription: String?, modifier: Modifier) { |
||||
AsyncImage( |
||||
model = ImageRequest.Builder(LocalContext.current) |
||||
.data(imageUrl) |
||||
.crossfade(true) |
||||
.build(), |
||||
contentDescription = contentDescription, |
||||
placeholder = painterResource(R.drawable.placeholder), |
||||
modifier = Modifier.fillMaxSize(), |
||||
contentScale = ContentScale.Crop, |
||||
) |
||||
} |
@ -0,0 +1,9 @@
|
||||
package com.example.jetsnack.ui.home |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.window.Dialog |
||||
|
||||
@Composable |
||||
actual fun SnackDialog(onCloseRequest: () -> Unit, content: @Composable () -> Unit) { |
||||
Dialog(onDismissRequest = onCloseRequest, content = content) |
||||
} |
@ -0,0 +1,189 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.home.cart |
||||
|
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.layout.* |
||||
import androidx.compose.material.Icon |
||||
import androidx.compose.material.IconButton |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.filled.Close |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.platform.LocalContext |
||||
import androidx.compose.ui.res.stringResource |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.constraintlayout.compose.ChainStyle |
||||
import androidx.constraintlayout.compose.ConstraintLayout |
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle |
||||
import androidx.lifecycle.viewmodel.compose.viewModel |
||||
import com.example.jetsnack.R |
||||
import com.example.jetsnack.model.OrderLine |
||||
import com.example.jetsnack.model.SnackRepo |
||||
import com.example.jetsnack.ui.components.JetsnackDivider |
||||
import com.example.jetsnack.ui.components.QuantitySelector |
||||
import com.example.jetsnack.ui.components.SnackImage |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
import com.example.jetsnack.ui.utils.formatPrice |
||||
|
||||
@Composable |
||||
actual fun provideCartViewModel(): CartViewModel { |
||||
return viewModel(factory = CartViewModel.provideFactory()) |
||||
} |
||||
|
||||
@Composable |
||||
actual fun ActualCartItem( |
||||
orderLine: OrderLine, |
||||
removeSnack: (Long) -> Unit, |
||||
increaseItemCount: (Long) -> Unit, |
||||
decreaseItemCount: (Long) -> Unit, |
||||
onSnackClick: (Long) -> Unit, |
||||
modifier: Modifier |
||||
) { |
||||
val snack = orderLine.snack |
||||
ConstraintLayout( |
||||
modifier = modifier |
||||
.fillMaxWidth() |
||||
.clickable { onSnackClick(snack.id) } |
||||
.background(JetsnackTheme.colors.uiBackground) |
||||
.padding(horizontal = 24.dp) |
||||
|
||||
) { |
||||
val (divider, image, name, tag, priceSpacer, price, remove, quantity) = createRefs() |
||||
createVerticalChain(name, tag, priceSpacer, price, chainStyle = ChainStyle.Packed) |
||||
SnackImage( |
||||
imageUrl = snack.imageUrl, |
||||
contentDescription = null, |
||||
modifier = Modifier |
||||
.size(100.dp) |
||||
.constrainAs(image) { |
||||
top.linkTo(parent.top, margin = 16.dp) |
||||
bottom.linkTo(parent.bottom, margin = 16.dp) |
||||
start.linkTo(parent.start) |
||||
} |
||||
) |
||||
Text( |
||||
text = snack.name, |
||||
style = MaterialTheme.typography.subtitle1, |
||||
color = JetsnackTheme.colors.textSecondary, |
||||
modifier = Modifier.constrainAs(name) { |
||||
linkTo( |
||||
start = image.end, |
||||
startMargin = 16.dp, |
||||
end = remove.start, |
||||
endMargin = 16.dp, |
||||
bias = 0f |
||||
) |
||||
} |
||||
) |
||||
IconButton( |
||||
onClick = { removeSnack(snack.id) }, |
||||
modifier = Modifier |
||||
.constrainAs(remove) { |
||||
top.linkTo(parent.top) |
||||
end.linkTo(parent.end) |
||||
} |
||||
.padding(top = 12.dp) |
||||
) { |
||||
Icon( |
||||
imageVector = Icons.Filled.Close, |
||||
tint = JetsnackTheme.colors.iconSecondary, |
||||
contentDescription = stringResource(R.string.label_remove) |
||||
) |
||||
} |
||||
Text( |
||||
text = snack.tagline, |
||||
style = MaterialTheme.typography.body1, |
||||
color = JetsnackTheme.colors.textHelp, |
||||
modifier = Modifier.constrainAs(tag) { |
||||
linkTo( |
||||
start = image.end, |
||||
startMargin = 16.dp, |
||||
end = parent.end, |
||||
endMargin = 16.dp, |
||||
bias = 0f |
||||
) |
||||
} |
||||
) |
||||
Spacer( |
||||
Modifier |
||||
.height(8.dp) |
||||
.constrainAs(priceSpacer) { |
||||
linkTo(top = tag.bottom, bottom = price.top) |
||||
} |
||||
) |
||||
Text( |
||||
text = formatPrice(snack.price), |
||||
style = MaterialTheme.typography.subtitle1, |
||||
color = JetsnackTheme.colors.textPrimary, |
||||
modifier = Modifier.constrainAs(price) { |
||||
linkTo( |
||||
start = image.end, |
||||
end = quantity.start, |
||||
startMargin = 16.dp, |
||||
endMargin = 16.dp, |
||||
bias = 0f |
||||
) |
||||
} |
||||
) |
||||
QuantitySelector( |
||||
count = orderLine.count, |
||||
decreaseItemCount = { decreaseItemCount(snack.id) }, |
||||
increaseItemCount = { increaseItemCount(snack.id) }, |
||||
modifier = Modifier.constrainAs(quantity) { |
||||
baseline.linkTo(price.baseline) |
||||
end.linkTo(parent.end) |
||||
} |
||||
) |
||||
JetsnackDivider( |
||||
Modifier.constrainAs(divider) { |
||||
linkTo(start = parent.start, end = parent.end) |
||||
top.linkTo(parent.bottom) |
||||
} |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun CartPreview() { |
||||
JetsnackTheme { |
||||
Cart( |
||||
orderLines = SnackRepo.getCart(), |
||||
removeSnack = {}, |
||||
increaseItemCount = {}, |
||||
decreaseItemCount = {}, |
||||
inspiredByCart = SnackRepo.getInspiredByCart(), |
||||
onSnackClick = {} |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
actual fun rememberQuantityString(res: Int, qty: Int, vararg args: Any): String { |
||||
val resources = LocalContext.current.resources |
||||
return remember(qty, resources) { resources.getQuantityString(R.plurals.cart_order_count, qty, qty) } |
||||
} |
||||
|
||||
@Composable |
||||
actual fun getCartContentInsets(): WindowInsets { |
||||
return WindowInsets.statusBars.add(WindowInsets(top = 56.dp)) |
||||
} |
@ -0,0 +1,48 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.home.cart |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.State |
||||
import androidx.lifecycle.ViewModel |
||||
import androidx.lifecycle.ViewModelProvider |
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle |
||||
import com.example.jetsnack.model.OrderLine |
||||
import com.example.jetsnack.model.SnackRepo |
||||
import com.example.jetsnack.model.SnackbarManager |
||||
import kotlinx.coroutines.flow.StateFlow |
||||
|
||||
/** |
||||
* Factory for CartViewModel that takes SnackbarManager as a dependency |
||||
*/ |
||||
fun CartViewModel.Companion.provideFactory( |
||||
snackbarManager: SnackbarManager = SnackbarManager, |
||||
snackRepository: SnackRepo = SnackRepo |
||||
): ViewModelProvider.Factory = object : ViewModelProvider.Factory { |
||||
@Suppress("UNCHECKED_CAST") |
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T { |
||||
return CartViewModel(snackbarManager, snackRepository) as T |
||||
} |
||||
} |
||||
|
||||
@OptIn(kotlin.ExperimentalMultiplatform::class) |
||||
actual abstract class JetSnackCartViewModel actual constructor() : ViewModel() { |
||||
@Composable |
||||
actual fun collectOrderLinesAsState(flow: StateFlow<List<OrderLine>>): State<List<OrderLine>> { |
||||
return flow.collectAsStateWithLifecycle() |
||||
} |
||||
} |
@ -0,0 +1,12 @@
|
||||
package com.example.jetsnack.ui.home |
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets |
||||
import androidx.compose.foundation.layout.add |
||||
import androidx.compose.foundation.layout.statusBars |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.unit.dp |
||||
|
||||
@Composable |
||||
actual fun snackCollectionListItemWindowInsets(): WindowInsets { |
||||
return WindowInsets.statusBars.add(WindowInsets(top = 56.dp)) |
||||
} |
@ -0,0 +1,10 @@
|
||||
package com.example.jetsnack.ui.snackdetail |
||||
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding |
||||
import androidx.compose.foundation.layout.statusBarsPadding |
||||
import androidx.compose.foundation.layout.systemBarsPadding |
||||
import androidx.compose.ui.Modifier |
||||
|
||||
actual fun Modifier.jetSnackNavigationBarsPadding(): Modifier = this.navigationBarsPadding() |
||||
actual fun Modifier.jetSnackStatusBarsPadding(): Modifier = this.statusBarsPadding() |
||||
actual fun Modifier.jetSnackSystemBarsPadding(): Modifier = this.systemBarsPadding() |
@ -0,0 +1,10 @@
|
||||
package com.example.jetsnack.ui.utils |
||||
|
||||
import java.math.BigDecimal |
||||
import java.text.NumberFormat |
||||
|
||||
actual fun formatPrice(price: Long): String { |
||||
return NumberFormat.getCurrencyInstance().format( |
||||
BigDecimal(price).movePointLeft(2) |
||||
) |
||||
} |
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<!-- |
||||
Copyright 2020 The Android Open Source Project |
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
in compliance with the License. You may obtain a copy of the License at |
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
or implied. See the License for the specific language governing permissions and limitations under |
||||
the License. |
||||
--> |
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:width="341dp" |
||||
android:height="179dp" |
||||
android:viewportWidth="341" |
||||
android:viewportHeight="179"> |
||||
<path |
||||
android:fillColor="#5F6368" |
||||
android:pathData="M302.676,111.056L244.424,65.728C234.123,57.654 224.238,49.061 214.807,39.98C198.202,24.102 175.659,11.407 149.414,4.648C85.649,-11.772 35.135,17.344 12.16,60.096C-22.949,125.426 20.921,195.341 105.817,175.009C145.621,169.5 174.324,161.356 200.455,154.855L295.072,135.285L302.676,111.056Z" /> |
||||
<path |
||||
android:fillColor="#3C4043" |
||||
android:pathData="M288.225,120.035a12.46,10.541 105,1 0,20.363 5.456a12.46,10.541 105,1 0,-20.363 -5.456z" /> |
||||
<path |
||||
android:fillColor="#ffffff" |
||||
android:pathData="M299.659,110.277C304.701,111.618 309.064,114.797 311.893,119.193L313.356,121.465L313.43,121.559L339.097,129.093C339.567,129.232 339.965,129.549 340.204,129.979C340.444,130.408 340.505,130.914 340.376,131.389L338.384,138.718C338.319,138.957 338.208,139.18 338.056,139.376C337.905,139.571 337.716,139.734 337.502,139.856C337.287,139.979 337.051,140.057 336.806,140.087C336.561,140.117 336.313,140.098 336.075,140.032L310.402,132.833L310.401,132.834L307.823,133.812C303.075,135.612 297.867,135.79 293.008,134.317V134.317L299.659,110.277Z" /> |
||||
<path |
||||
android:fillColor="#ffffff" |
||||
android:fillType="evenOdd" |
||||
android:pathData="M161.472,52.165L151.381,69.821V69.849C160.101,74.643 167.496,81.558 172.896,89.966C178.297,98.374 181.531,108.01 182.306,118H61C61.765,108.002 64.996,98.356 70.397,89.939C75.798,81.523 83.198,74.602 91.925,69.807L81.827,52.165C81.551,51.678 81.478,51.101 81.624,50.56C81.77,50.019 82.122,49.558 82.605,49.279C83.087,49.001 83.659,48.927 84.195,49.074C84.731,49.221 85.188,49.577 85.464,50.064L95.687,67.93C103.852,64.232 112.7,62.321 121.65,62.321C130.599,62.321 139.448,64.232 147.613,67.93L157.836,50.064C158.112,49.577 158.568,49.221 159.104,49.074C159.64,48.927 160.213,49.001 160.695,49.279C161.177,49.558 161.53,50.019 161.676,50.56C161.822,51.101 161.748,51.678 161.472,52.165ZM133.338,84.859C133.338,79.463 128.696,75.709 121.95,75.709C116.815,75.709 113.167,77.774 111.438,81.052C110.345,83.124 111.889,85.617 114.226,85.617C114.833,85.623 115.428,85.455 115.943,85.133C116.457,84.81 116.869,84.346 117.129,83.797C117.868,82.172 119.481,81.177 121.518,81.177C124.199,81.177 126.358,82.82 126.358,85.058C126.358,87.296 125.08,88.451 122.04,90.273C118.783,92.186 117.488,94.496 117.794,98.214L117.8,98.39C117.813,98.76 117.968,99.109 118.233,99.366C118.498,99.623 118.852,99.766 119.22,99.766H122.669C122.856,99.766 123.041,99.729 123.213,99.658C123.386,99.586 123.543,99.481 123.675,99.349C123.806,99.216 123.911,99.059 123.983,98.886C124.054,98.713 124.091,98.528 124.091,98.341C124.091,96.031 125.152,94.695 128.283,92.872C131.611,90.905 133.338,88.433 133.338,84.859ZM121.068,102.925C118.945,102.925 117.218,104.567 117.218,106.642C117.218,108.736 118.927,110.36 121.068,110.36C123.209,110.36 124.936,108.736 124.936,106.642C124.936,104.549 123.209,102.925 121.068,102.925Z" /> |
||||
</vector> |
After Width: | Height: | Size: 38 KiB |
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<!-- |
||||
Copyright 2020 The Android Open Source Project |
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
in compliance with the License. You may obtain a copy of the License at |
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
or implied. See the License for the specific language governing permissions and limitations under |
||||
the License. |
||||
--> |
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:aapt="http://schemas.android.com/aapt" |
||||
android:width="108dp" |
||||
android:height="108dp" |
||||
android:viewportWidth="108" |
||||
android:viewportHeight="108"> |
||||
<path |
||||
android:pathData="M70,36v36h-3.6L66.4,57.6L61,57.6L61,43.2c0,-3.168 4.032,-7.2 9,-7.2zM40.6,36v12.6h3.6L44.2,36h3.6v12.6h3.6L51.4,36L55,36v12.6c0,3.978 -3.222,7.2 -7.2,7.2L47.8,72h-3.6L44.2,55.8a7.198,7.198 0,0 1,-7.2 -7.2L37,36h3.6z" |
||||
android:fillColor="#fff" |
||||
android:fillType="evenOdd"/> |
||||
<path |
||||
android:pathData="M0,0h108v108H0z" |
||||
android:fillType="evenOdd"> |
||||
<aapt:attr name="android:fillColor"> |
||||
<gradient |
||||
android:gradientRadius="71.14824" |
||||
android:centerX="29.74104" |
||||
android:centerY="29.68488" |
||||
android:type="radial"> |
||||
<item android:offset="0" android:color="#19ffffff"/> |
||||
<item android:offset="1" android:color="#00ffffff"/> |
||||
</gradient> |
||||
</aapt:attr> |
||||
</path> |
||||
</vector> |
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<!-- |
||||
Copyright 2020 The Android Open Source Project |
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
in compliance with the License. You may obtain a copy of the License at |
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
or implied. See the License for the specific language governing permissions and limitations under |
||||
the License. |
||||
--> |
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:width="341dp" |
||||
android:height="179dp" |
||||
android:viewportWidth="341" |
||||
android:viewportHeight="179"> |
||||
<path |
||||
android:fillColor="#DDE3E8" |
||||
android:pathData="M302.676,111.056L244.424,65.728C234.123,57.654 224.238,49.061 214.807,39.98C198.202,24.102 175.659,11.407 149.414,4.648C85.649,-11.772 35.135,17.344 12.16,60.096C-22.949,125.426 20.921,195.341 105.817,175.009C145.621,169.5 174.324,161.356 200.455,154.855L295.072,135.285L302.676,111.056Z" /> |
||||
<path |
||||
android:fillColor="#ffffff" |
||||
android:pathData="M288.225,120.035a12.46,10.541 105,1 0,20.363 5.456a12.46,10.541 105,1 0,-20.363 -5.456z" /> |
||||
<path |
||||
android:fillColor="#3C4043" |
||||
android:pathData="M299.659,110.277C304.701,111.618 309.064,114.797 311.893,119.193L313.356,121.465L313.43,121.559L339.097,129.093C339.567,129.232 339.965,129.549 340.204,129.979C340.444,130.408 340.505,130.914 340.376,131.389L338.384,138.718C338.319,138.957 338.208,139.18 338.056,139.376C337.905,139.571 337.716,139.734 337.502,139.856C337.287,139.979 337.051,140.057 336.806,140.087C336.561,140.117 336.313,140.098 336.075,140.032L310.402,132.833L310.401,132.834L307.823,133.812C303.075,135.612 297.867,135.79 293.008,134.317V134.317L299.659,110.277Z" /> |
||||
<path |
||||
android:fillColor="#3C4043" |
||||
android:fillType="evenOdd" |
||||
android:pathData="M161.472,52.165L151.381,69.821V69.849C160.101,74.643 167.496,81.558 172.896,89.966C178.297,98.374 181.531,108.01 182.306,118H61C61.765,108.002 64.996,98.356 70.397,89.939C75.798,81.523 83.198,74.602 91.925,69.807L81.827,52.165C81.551,51.678 81.478,51.101 81.624,50.56C81.77,50.019 82.122,49.558 82.605,49.279C83.087,49.001 83.659,48.927 84.195,49.074C84.731,49.221 85.188,49.577 85.464,50.064L95.687,67.93C103.852,64.232 112.7,62.321 121.65,62.321C130.599,62.321 139.448,64.232 147.613,67.93L157.836,50.064C158.112,49.577 158.568,49.221 159.104,49.074C159.64,48.927 160.213,49.001 160.695,49.279C161.177,49.558 161.53,50.019 161.676,50.56C161.822,51.101 161.748,51.678 161.472,52.165ZM133.338,84.859C133.338,79.463 128.696,75.709 121.95,75.709C116.815,75.709 113.167,77.774 111.438,81.052C110.345,83.124 111.889,85.617 114.226,85.617C114.833,85.623 115.428,85.455 115.943,85.133C116.457,84.81 116.869,84.346 117.129,83.797C117.868,82.172 119.481,81.177 121.518,81.177C124.199,81.177 126.358,82.82 126.358,85.058C126.358,87.296 125.08,88.451 122.04,90.273C118.783,92.186 117.488,94.496 117.794,98.214L117.8,98.39C117.813,98.76 117.968,99.109 118.233,99.366C118.498,99.623 118.852,99.766 119.22,99.766H122.669C122.856,99.766 123.041,99.729 123.213,99.658C123.386,99.586 123.543,99.481 123.675,99.349C123.806,99.216 123.911,99.059 123.983,98.886C124.054,98.713 124.091,98.528 124.091,98.341C124.091,96.031 125.152,94.695 128.283,92.872C131.611,90.905 133.338,88.433 133.338,84.859ZM121.068,102.925C118.945,102.925 117.218,104.567 117.218,106.642C117.218,108.736 118.927,110.36 121.068,110.36C123.209,110.36 124.936,108.736 124.936,106.642C124.936,104.549 123.209,102.925 121.068,102.925Z" /> |
||||
</vector> |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<!-- |
||||
Copyright 2020 The Android Open Source Project |
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
in compliance with the License. You may obtain a copy of the License at |
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
or implied. See the License for the specific language governing permissions and limitations under |
||||
the License. |
||||
--> |
||||
<resources> |
||||
|
||||
<style name="Theme.Material.DayNight.NoActionBar" parent="@android:style/Theme.Material.NoActionBar" /> |
||||
|
||||
</resources> |
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<!-- |
||||
Copyright 2020 The Android Open Source Project |
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
in compliance with the License. You may obtain a copy of the License at |
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
or implied. See the License for the specific language governing permissions and limitations under |
||||
the License. |
||||
--> |
||||
<resources> |
||||
<color name="shadow_5">#ff4b30ed</color> |
||||
</resources> |
@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<!-- |
||||
Copyright 2020 The Android Open Source Project |
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
in compliance with the License. You may obtain a copy of the License at |
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
or implied. See the License for the specific language governing permissions and limitations under |
||||
the License. |
||||
--> |
||||
<resources> |
||||
<string name="app_name">Jetsnack</string> |
||||
<string name="label_back">Back</string> |
||||
|
||||
<!-- Home Tabs --> |
||||
<string name="home_feed">Home</string> |
||||
<string name="home_search">Search</string> |
||||
<string name="home_cart">My Cart</string> |
||||
<string name="home_profile">Profile</string> |
||||
|
||||
<!-- Home --> |
||||
<string name="label_filters">Filters</string> |
||||
<string name="label_select_delivery">Select delivery address</string> |
||||
|
||||
<!-- Search --> |
||||
<string name="search_jetsnack">Search Jetsnack</string> |
||||
<string name="search_no_matches">No matches for “%1s”</string> |
||||
<string name="search_no_matches_retry">Try broadening your search</string> |
||||
<string name="search_count">%1d items</string> |
||||
<string name="label_add">Add to cart</string> |
||||
<string name="label_search">Perform search</string> |
||||
|
||||
<!-- Snack Detail --> |
||||
<string name="detail_header">Details</string> |
||||
<string name="detail_placeholder">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut tempus, sem vitae convallis imperdiet, lectus nunc pharetra diam, ac rhoncus quam eros eu risus. Nulla pulvinar condimentum erat, pulvinar tempus turpis blandit ut. Etiam sed ipsum sed lacus eleifend hendrerit eu quis quam. Etiam ligula eros, finibus vestibulum tortor ac, ultrices accumsan dolor. Vivamus vel nisl a libero lobortis posuere. Aenean facilisis nibh vel ultrices bibendum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse ac est vitae lacus commodo efficitur at ut massa. Etiam vestibulum sit amet sapien sed varius. Aliquam non ipsum imperdiet, pulvinar enim nec, mollis risus. Fusce id tincidunt nisl.</string> |
||||
<string name="ingredients">Ingredients</string> |
||||
<string name="ingredients_list">Vanilla, Almond Flour, Eggs, Butter, Cream, Sugar</string> |
||||
<string name="quantity">Qty</string> |
||||
<string name="add_to_cart">ADD TO CART</string> |
||||
|
||||
<!-- Cart --> |
||||
<string name="cart_order_header">Order (%1s)</string> |
||||
<plurals name="cart_order_count"> |
||||
<item quantity="one">%1d item</item> |
||||
<item quantity="other">%1d items</item> |
||||
</plurals> |
||||
<string name="cart_summary_header">Summary</string> |
||||
<string name="cart_subtotal_label">Subtotal</string> |
||||
<string name="cart_shipping_label">Shipping & Handling</string> |
||||
<string name="cart_total_label">Total</string> |
||||
<string name="cart_checkout">Checkout</string> |
||||
<string name="cart_increase_error">There was an error and the quantity couldn\'t be increased. Please try again.</string> |
||||
<string name="cart_decrease_error">There was an error and the quantity couldn\'t be decreased. Please try again.</string> |
||||
<string name="label_remove">Remove item</string> |
||||
|
||||
<!-- Quantity Selector --> |
||||
<string name="label_increase">Increase</string> |
||||
<string name="label_decrease">Decrease</string> |
||||
<string name="work_in_progress">This is currently work in progress</string> |
||||
<string name="grab_beverage">Grab a beverage and check back later!</string> |
||||
<string name="see_more">SEE MORE</string> |
||||
<string name="see_less">SEE LESS</string> |
||||
<string name="remove_item">Remove Item</string> |
||||
<string name="reset">Reset</string> |
||||
<string name="sort">Sort</string> |
||||
<string name="price">Price</string> |
||||
<string name="category">Category</string> |
||||
<string name="max_calories">Max Calories</string> |
||||
<string name="lifestyle">LifeStyle</string> |
||||
<string name="per_serving">per serving</string> |
||||
<string name="android_favorites">Android\'s Favorite (default)</string> |
||||
<string name="rating">Rating</string> |
||||
<string name="alphabetical">Alphabetical</string> |
||||
<string name="close">Close</string> |
||||
|
||||
</resources> |
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<!-- |
||||
Copyright 2020 The Android Open Source Project |
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
in compliance with the License. You may obtain a copy of the License at |
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
or implied. See the License for the specific language governing permissions and limitations under |
||||
the License. |
||||
--> |
||||
<resources> |
||||
|
||||
<style name="Theme.Jetsnack" parent="Theme.Material.DayNight.NoActionBar"> |
||||
<item name="android:colorPrimary">#ff00ff</item> |
||||
<item name="android:colorAccent">#ff00ff</item> |
||||
<item name="android:statusBarColor">@android:color/transparent</item> |
||||
<item name="android:navigationBarColor">@android:color/transparent</item> |
||||
<item name="android:dialogTheme">@style/Theme.DialogFullScreen</item> |
||||
</style> |
||||
|
||||
<style name="Theme.Material.DayNight.NoActionBar" parent="@android:style/Theme.Material.Light.NoActionBar" /> |
||||
|
||||
<style name="Theme.DialogFullScreen" parent="Theme.Material.DayNight.NoActionBar"> |
||||
<item name="android:windowMinWidthMajor">100%</item> |
||||
<item name="android:windowMinWidthMinor">100%</item> |
||||
</style> |
||||
</resources> |
@ -0,0 +1,220 @@
|
||||
package com.example.jetsnack |
||||
|
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.materialIcon |
||||
import androidx.compose.material.icons.materialPath |
||||
import androidx.compose.ui.graphics.vector.ImageVector |
||||
|
||||
public val Icons.Filled.Android: ImageVector |
||||
get() { |
||||
if (_android != null) { |
||||
return _android!! |
||||
} |
||||
_android = materialIcon(name = "Filled.Android") { |
||||
materialPath { |
||||
moveTo(17.6f, 9.48f) |
||||
lineToRelative(1.84f, -3.18f) |
||||
curveToRelative(0.16f, -0.31f, 0.04f, -0.69f, -0.26f, -0.85f) |
||||
curveToRelative(-0.29f, -0.15f, -0.65f, -0.06f, -0.83f, 0.22f) |
||||
lineToRelative(-1.88f, 3.24f) |
||||
curveToRelative(-2.86f, -1.21f, -6.08f, -1.21f, -8.94f, 0.0f) |
||||
lineTo(5.65f, 5.67f) |
||||
curveToRelative(-0.19f, -0.29f, -0.58f, -0.38f, -0.87f, -0.2f) |
||||
curveTo(4.5f, 5.65f, 4.41f, 6.01f, 4.56f, 6.3f) |
||||
lineTo(6.4f, 9.48f) |
||||
curveTo(3.3f, 11.25f, 1.28f, 14.44f, 1.0f, 18.0f) |
||||
horizontalLineToRelative(22.0f) |
||||
curveTo(22.72f, 14.44f, 20.7f, 11.25f, 17.6f, 9.48f) |
||||
close() |
||||
moveTo(7.0f, 15.25f) |
||||
curveToRelative(-0.69f, 0.0f, -1.25f, -0.56f, -1.25f, -1.25f) |
||||
curveToRelative(0.0f, -0.69f, 0.56f, -1.25f, 1.25f, -1.25f) |
||||
reflectiveCurveTo(8.25f, 13.31f, 8.25f, 14.0f) |
||||
curveTo(8.25f, 14.69f, 7.69f, 15.25f, 7.0f, 15.25f) |
||||
close() |
||||
moveTo(17.0f, 15.25f) |
||||
curveToRelative(-0.69f, 0.0f, -1.25f, -0.56f, -1.25f, -1.25f) |
||||
curveToRelative(0.0f, -0.69f, 0.56f, -1.25f, 1.25f, -1.25f) |
||||
reflectiveCurveToRelative(1.25f, 0.56f, 1.25f, 1.25f) |
||||
curveTo(18.25f, 14.69f, 17.69f, 15.25f, 17.0f, 15.25f) |
||||
close() |
||||
} |
||||
} |
||||
return _android!! |
||||
} |
||||
|
||||
private var _android: ImageVector? = null |
||||
|
||||
public val Icons.Filled.SortByAlpha: ImageVector |
||||
get() { |
||||
if (_sortByAlpha != null) { |
||||
return _sortByAlpha!! |
||||
} |
||||
_sortByAlpha = materialIcon(name = "Filled.SortByAlpha") { |
||||
materialPath { |
||||
moveTo(14.94f, 4.66f) |
||||
horizontalLineToRelative(-4.72f) |
||||
lineToRelative(2.36f, -2.36f) |
||||
close() |
||||
moveTo(10.25f, 19.37f) |
||||
horizontalLineToRelative(4.66f) |
||||
lineToRelative(-2.33f, 2.33f) |
||||
close() |
||||
moveTo(6.1f, 6.27f) |
||||
lineTo(1.6f, 17.73f) |
||||
horizontalLineToRelative(1.84f) |
||||
lineToRelative(0.92f, -2.45f) |
||||
horizontalLineToRelative(5.11f) |
||||
lineToRelative(0.92f, 2.45f) |
||||
horizontalLineToRelative(1.84f) |
||||
lineTo(7.74f, 6.27f) |
||||
lineTo(6.1f, 6.27f) |
||||
close() |
||||
moveTo(4.97f, 13.64f) |
||||
lineToRelative(1.94f, -5.18f) |
||||
lineToRelative(1.94f, 5.18f) |
||||
lineTo(4.97f, 13.64f) |
||||
close() |
||||
moveTo(15.73f, 16.14f) |
||||
horizontalLineToRelative(6.12f) |
||||
verticalLineToRelative(1.59f) |
||||
horizontalLineToRelative(-8.53f) |
||||
verticalLineToRelative(-1.29f) |
||||
lineToRelative(5.92f, -8.56f) |
||||
horizontalLineToRelative(-5.88f) |
||||
verticalLineToRelative(-1.6f) |
||||
horizontalLineToRelative(8.3f) |
||||
verticalLineToRelative(1.26f) |
||||
lineToRelative(-5.93f, 8.6f) |
||||
close() |
||||
} |
||||
} |
||||
return _sortByAlpha!! |
||||
} |
||||
|
||||
private var _sortByAlpha: ImageVector? = null |
||||
|
||||
public val Icons.Rounded.FilterList: ImageVector |
||||
get() { |
||||
if (_filterList != null) { |
||||
return _filterList!! |
||||
} |
||||
_filterList = materialIcon(name = "Rounded.FilterList") { |
||||
materialPath { |
||||
moveTo(11.0f, 18.0f) |
||||
horizontalLineToRelative(2.0f) |
||||
curveToRelative(0.55f, 0.0f, 1.0f, -0.45f, 1.0f, -1.0f) |
||||
reflectiveCurveToRelative(-0.45f, -1.0f, -1.0f, -1.0f) |
||||
horizontalLineToRelative(-2.0f) |
||||
curveToRelative(-0.55f, 0.0f, -1.0f, 0.45f, -1.0f, 1.0f) |
||||
reflectiveCurveToRelative(0.45f, 1.0f, 1.0f, 1.0f) |
||||
close() |
||||
moveTo(3.0f, 7.0f) |
||||
curveToRelative(0.0f, 0.55f, 0.45f, 1.0f, 1.0f, 1.0f) |
||||
horizontalLineToRelative(16.0f) |
||||
curveToRelative(0.55f, 0.0f, 1.0f, -0.45f, 1.0f, -1.0f) |
||||
reflectiveCurveToRelative(-0.45f, -1.0f, -1.0f, -1.0f) |
||||
lineTo(4.0f, 6.0f) |
||||
curveToRelative(-0.55f, 0.0f, -1.0f, 0.45f, -1.0f, 1.0f) |
||||
close() |
||||
moveTo(7.0f, 13.0f) |
||||
horizontalLineToRelative(10.0f) |
||||
curveToRelative(0.55f, 0.0f, 1.0f, -0.45f, 1.0f, -1.0f) |
||||
reflectiveCurveToRelative(-0.45f, -1.0f, -1.0f, -1.0f) |
||||
lineTo(7.0f, 11.0f) |
||||
curveToRelative(-0.55f, 0.0f, -1.0f, 0.45f, -1.0f, 1.0f) |
||||
reflectiveCurveToRelative(0.45f, 1.0f, 1.0f, 1.0f) |
||||
close() |
||||
} |
||||
} |
||||
return _filterList!! |
||||
} |
||||
|
||||
private var _filterList: ImageVector? = null |
||||
|
||||
public val Icons.Filled.Remove: ImageVector |
||||
get() { |
||||
if (_remove != null) { |
||||
return _remove!! |
||||
} |
||||
_remove = materialIcon(name = "Filled.Remove") { |
||||
materialPath { |
||||
moveTo(19.0f, 13.0f) |
||||
horizontalLineTo(5.0f) |
||||
verticalLineToRelative(-2.0f) |
||||
horizontalLineToRelative(14.0f) |
||||
verticalLineToRelative(2.0f) |
||||
close() |
||||
} |
||||
} |
||||
return _remove!! |
||||
} |
||||
|
||||
private var _remove: ImageVector? = null |
||||
|
||||
public val Icons.Outlined.ExpandMore: ImageVector |
||||
get() { |
||||
if (_expandMore != null) { |
||||
return _expandMore!! |
||||
} |
||||
_expandMore = materialIcon(name = "Outlined.ExpandMore") { |
||||
materialPath { |
||||
moveTo(16.59f, 8.59f) |
||||
lineTo(12.0f, 13.17f) |
||||
lineTo(7.41f, 8.59f) |
||||
lineTo(6.0f, 10.0f) |
||||
lineToRelative(6.0f, 6.0f) |
||||
lineToRelative(6.0f, -6.0f) |
||||
lineToRelative(-1.41f, -1.41f) |
||||
close() |
||||
} |
||||
} |
||||
return _expandMore!! |
||||
} |
||||
|
||||
private var _expandMore: ImageVector? = null |
||||
|
||||
public val Icons.Filled.DeleteForever: ImageVector |
||||
get() { |
||||
if (_deleteForever != null) { |
||||
return _deleteForever!! |
||||
} |
||||
_deleteForever = materialIcon(name = "Filled.DeleteForever") { |
||||
materialPath { |
||||
moveTo(6.0f, 19.0f) |
||||
curveToRelative(0.0f, 1.1f, 0.9f, 2.0f, 2.0f, 2.0f) |
||||
horizontalLineToRelative(8.0f) |
||||
curveToRelative(1.1f, 0.0f, 2.0f, -0.9f, 2.0f, -2.0f) |
||||
lineTo(18.0f, 7.0f) |
||||
lineTo(6.0f, 7.0f) |
||||
verticalLineToRelative(12.0f) |
||||
close() |
||||
moveTo(8.46f, 11.88f) |
||||
lineToRelative(1.41f, -1.41f) |
||||
lineTo(12.0f, 12.59f) |
||||
lineToRelative(2.12f, -2.12f) |
||||
lineToRelative(1.41f, 1.41f) |
||||
lineTo(13.41f, 14.0f) |
||||
lineToRelative(2.12f, 2.12f) |
||||
lineToRelative(-1.41f, 1.41f) |
||||
lineTo(12.0f, 15.41f) |
||||
lineToRelative(-2.12f, 2.12f) |
||||
lineToRelative(-1.41f, -1.41f) |
||||
lineTo(10.59f, 14.0f) |
||||
lineToRelative(-2.13f, -2.12f) |
||||
close() |
||||
moveTo(15.5f, 4.0f) |
||||
lineToRelative(-1.0f, -1.0f) |
||||
horizontalLineToRelative(-5.0f) |
||||
lineToRelative(-1.0f, 1.0f) |
||||
lineTo(5.0f, 4.0f) |
||||
verticalLineToRelative(2.0f) |
||||
horizontalLineToRelative(14.0f) |
||||
lineTo(19.0f, 4.0f) |
||||
close() |
||||
} |
||||
} |
||||
return _deleteForever!! |
||||
} |
||||
|
||||
private var _deleteForever: ImageVector? = null |
@ -0,0 +1,9 @@
|
||||
package com.example.jetsnack |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.graphics.painter.Painter |
||||
|
||||
@Composable |
||||
expect fun painterResource(id: Int): Painter |
||||
|
||||
expect val MppR.drawable.empty_state_search: Int |
@ -0,0 +1,87 @@
|
||||
package com.example.jetsnack |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
|
||||
@Composable |
||||
expect fun stringResource(id: Int): String |
||||
|
||||
@Composable |
||||
expect fun stringResource(id: Int, part: String): String |
||||
|
||||
@Composable |
||||
expect fun stringResource(id: Int, count: Int): String |
||||
|
||||
object MppR { |
||||
object string {} |
||||
|
||||
object drawable {} |
||||
|
||||
object plurals {} |
||||
} |
||||
|
||||
expect val MppR.plurals.cart_order_count: Int |
||||
|
||||
// Filters |
||||
expect val MppR.string.label_filters: Int |
||||
|
||||
// Qty |
||||
expect val MppR.string.quantity: Int |
||||
expect val MppR.string.label_decrease: Int |
||||
expect val MppR.string.label_increase: Int |
||||
|
||||
|
||||
// Snack detail |
||||
expect val MppR.string.label_back: Int |
||||
expect val MppR.string.detail_header: Int |
||||
expect val MppR.string.detail_placeholder: Int |
||||
expect val MppR.string.see_more: Int |
||||
expect val MppR.string.see_less: Int |
||||
expect val MppR.string.ingredients: Int |
||||
expect val MppR.string.ingredients_list: Int |
||||
expect val MppR.string.add_to_cart: Int |
||||
|
||||
// Home |
||||
|
||||
expect val MppR.string.label_select_delivery: Int |
||||
|
||||
// Filter |
||||
expect val MppR.string.max_calories: Int |
||||
expect val MppR.string.per_serving: Int |
||||
expect val MppR.string.sort: Int |
||||
expect val MppR.string.lifestyle: Int |
||||
expect val MppR.string.category: Int |
||||
expect val MppR.string.price: Int |
||||
expect val MppR.string.reset: Int |
||||
expect val MppR.string.close: Int |
||||
|
||||
// Profile |
||||
|
||||
expect val MppR.string.work_in_progress: Int |
||||
expect val MppR.string.grab_beverage: Int |
||||
|
||||
// Home |
||||
expect val MppR.string.home_feed: Int |
||||
expect val MppR.string.home_search: Int |
||||
expect val MppR.string.home_cart: Int |
||||
expect val MppR.string.home_profile: Int |
||||
|
||||
// Search |
||||
expect val MppR.string.search_no_matches: Int |
||||
expect val MppR.string.search_no_matches_retry: Int |
||||
expect val MppR.string.label_add: Int |
||||
expect val MppR.string.search_count: Int |
||||
expect val MppR.string.label_search: Int |
||||
expect val MppR.string.search_jetsnack: Int |
||||
|
||||
expect val MppR.string.cart_increase_error: Int |
||||
expect val MppR.string.cart_decrease_error: Int |
||||
|
||||
// Cart |
||||
expect val MppR.string.cart_order_header: Int |
||||
expect val MppR.string.remove_item: Int |
||||
expect val MppR.string.cart_summary_header: Int |
||||
expect val MppR.string.cart_subtotal_label: Int |
||||
expect val MppR.string.cart_shipping_label: Int |
||||
expect val MppR.string.cart_total_label: Int |
||||
expect val MppR.string.cart_checkout: Int |
||||
expect val MppR.string.label_remove: Int |
@ -0,0 +1,337 @@
|
||||
package com.example.jetsnack.flowlayout |
||||
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.layout.Layout |
||||
import androidx.compose.ui.layout.Placeable |
||||
import androidx.compose.ui.unit.Constraints |
||||
import androidx.compose.ui.unit.Dp |
||||
import androidx.compose.ui.unit.IntSize |
||||
import androidx.compose.ui.unit.LayoutDirection |
||||
import androidx.compose.ui.unit.dp |
||||
import kotlin.math.max |
||||
|
||||
internal enum class LayoutOrientation { |
||||
Horizontal, |
||||
Vertical |
||||
} |
||||
|
||||
internal data class OrientationIndependentConstraints( |
||||
val mainAxisMin: Int, |
||||
val mainAxisMax: Int, |
||||
val crossAxisMin: Int, |
||||
val crossAxisMax: Int |
||||
) { |
||||
constructor(c: Constraints, orientation: LayoutOrientation) : this( |
||||
if (orientation === LayoutOrientation.Horizontal) c.minWidth else c.minHeight, |
||||
if (orientation === LayoutOrientation.Horizontal) c.maxWidth else c.maxHeight, |
||||
if (orientation === LayoutOrientation.Horizontal) c.minHeight else c.minWidth, |
||||
if (orientation === LayoutOrientation.Horizontal) c.maxHeight else c.maxWidth |
||||
) |
||||
} |
||||
|
||||
/** |
||||
* A composable that places its children in a horizontal flow. Unlike [Row], if the |
||||
* horizontal space is too small to put all the children in one row, multiple rows may be used. |
||||
* |
||||
* Note that just like [Row], flex values cannot be used with [FlowRow]. |
||||
* |
||||
* @param modifier The modifier to be applied to the FlowRow. |
||||
* @param mainAxisSize The size of the layout in the main axis direction. |
||||
* @param mainAxisAlignment The alignment of each row's children in the main axis direction. |
||||
* @param mainAxisSpacing The main axis spacing between the children of each row. |
||||
* @param crossAxisAlignment The alignment of each row's children in the cross axis direction. |
||||
* @param crossAxisSpacing The cross axis spacing between the rows of the layout. |
||||
* @param lastLineMainAxisAlignment Overrides the main axis alignment of the last row. |
||||
*/ |
||||
@Composable |
||||
public fun FlowRow( |
||||
modifier: Modifier = Modifier, |
||||
mainAxisSize: SizeMode = SizeMode.Wrap, |
||||
mainAxisAlignment: FlowMainAxisAlignment = FlowMainAxisAlignment.Start, |
||||
mainAxisSpacing: Dp = 0.dp, |
||||
crossAxisAlignment: FlowCrossAxisAlignment = FlowCrossAxisAlignment.Start, |
||||
crossAxisSpacing: Dp = 0.dp, |
||||
lastLineMainAxisAlignment: FlowMainAxisAlignment = mainAxisAlignment, |
||||
content: @Composable () -> Unit |
||||
) { |
||||
Flow( |
||||
modifier = modifier, |
||||
orientation = LayoutOrientation.Horizontal, |
||||
mainAxisSize = mainAxisSize, |
||||
mainAxisAlignment = mainAxisAlignment, |
||||
mainAxisSpacing = mainAxisSpacing, |
||||
crossAxisAlignment = crossAxisAlignment, |
||||
crossAxisSpacing = crossAxisSpacing, |
||||
lastLineMainAxisAlignment = lastLineMainAxisAlignment, |
||||
content = content |
||||
) |
||||
} |
||||
|
||||
/** |
||||
* A composable that places its children in a vertical flow. Unlike [Column], if the |
||||
* vertical space is too small to put all the children in one column, multiple columns may be used. |
||||
* |
||||
* Note that just like [Column], flex values cannot be used with [FlowColumn]. |
||||
* |
||||
* @param modifier The modifier to be applied to the FlowColumn. |
||||
* @param mainAxisSize The size of the layout in the main axis direction. |
||||
* @param mainAxisAlignment The alignment of each column's children in the main axis direction. |
||||
* @param mainAxisSpacing The main axis spacing between the children of each column. |
||||
* @param crossAxisAlignment The alignment of each column's children in the cross axis direction. |
||||
* @param crossAxisSpacing The cross axis spacing between the columns of the layout. |
||||
* @param lastLineMainAxisAlignment Overrides the main axis alignment of the last column. |
||||
*/ |
||||
@Composable |
||||
public fun FlowColumn( |
||||
modifier: Modifier = Modifier, |
||||
mainAxisSize: SizeMode = SizeMode.Wrap, |
||||
mainAxisAlignment: FlowMainAxisAlignment = FlowMainAxisAlignment.Start, |
||||
mainAxisSpacing: Dp = 0.dp, |
||||
crossAxisAlignment: FlowCrossAxisAlignment = FlowCrossAxisAlignment.Start, |
||||
crossAxisSpacing: Dp = 0.dp, |
||||
lastLineMainAxisAlignment: FlowMainAxisAlignment = mainAxisAlignment, |
||||
content: @Composable () -> Unit |
||||
) { |
||||
Flow( |
||||
modifier = modifier, |
||||
orientation = LayoutOrientation.Vertical, |
||||
mainAxisSize = mainAxisSize, |
||||
mainAxisAlignment = mainAxisAlignment, |
||||
mainAxisSpacing = mainAxisSpacing, |
||||
crossAxisAlignment = crossAxisAlignment, |
||||
crossAxisSpacing = crossAxisSpacing, |
||||
lastLineMainAxisAlignment = lastLineMainAxisAlignment, |
||||
content = content |
||||
) |
||||
} |
||||
|
||||
/** |
||||
* Used to specify the alignment of a layout's children, in cross axis direction. |
||||
*/ |
||||
public enum class FlowCrossAxisAlignment { |
||||
/** |
||||
* Place children such that their center is in the middle of the cross axis. |
||||
*/ |
||||
Center, |
||||
/** |
||||
* Place children such that their start edge is aligned to the start edge of the cross axis. |
||||
*/ |
||||
Start, |
||||
/** |
||||
* Place children such that their end edge is aligned to the end edge of the cross axis. |
||||
*/ |
||||
End, |
||||
} |
||||
|
||||
public typealias FlowMainAxisAlignment = MainAxisAlignment |
||||
|
||||
/** |
||||
* Layout model that arranges its children in a horizontal or vertical flow. |
||||
*/ |
||||
@Composable |
||||
private fun Flow( |
||||
modifier: Modifier, |
||||
orientation: LayoutOrientation, |
||||
mainAxisSize: SizeMode, |
||||
mainAxisAlignment: FlowMainAxisAlignment, |
||||
mainAxisSpacing: Dp, |
||||
crossAxisAlignment: FlowCrossAxisAlignment, |
||||
crossAxisSpacing: Dp, |
||||
lastLineMainAxisAlignment: FlowMainAxisAlignment, |
||||
content: @Composable () -> Unit |
||||
) { |
||||
fun Placeable.mainAxisSize() = |
||||
if (orientation == LayoutOrientation.Horizontal) width else height |
||||
fun Placeable.crossAxisSize() = |
||||
if (orientation == LayoutOrientation.Horizontal) height else width |
||||
|
||||
Layout(content, modifier) { measurables, outerConstraints -> |
||||
val sequences = mutableListOf<List<Placeable>>() |
||||
val crossAxisSizes = mutableListOf<Int>() |
||||
val crossAxisPositions = mutableListOf<Int>() |
||||
|
||||
var mainAxisSpace = 0 |
||||
var crossAxisSpace = 0 |
||||
|
||||
val currentSequence = mutableListOf<Placeable>() |
||||
var currentMainAxisSize = 0 |
||||
var currentCrossAxisSize = 0 |
||||
|
||||
val constraints = OrientationIndependentConstraints(outerConstraints, orientation) |
||||
|
||||
val childConstraints = if (orientation == LayoutOrientation.Horizontal) { |
||||
Constraints(maxWidth = constraints.mainAxisMax) |
||||
} else { |
||||
Constraints(maxHeight = constraints.mainAxisMax) |
||||
} |
||||
|
||||
// Return whether the placeable can be added to the current sequence. |
||||
fun canAddToCurrentSequence(placeable: Placeable) = |
||||
currentSequence.isEmpty() || currentMainAxisSize + mainAxisSpacing.roundToPx() + |
||||
placeable.mainAxisSize() <= constraints.mainAxisMax |
||||
|
||||
// Store current sequence information and start a new sequence. |
||||
fun startNewSequence() { |
||||
if (sequences.isNotEmpty()) { |
||||
crossAxisSpace += crossAxisSpacing.roundToPx() |
||||
} |
||||
sequences += currentSequence.toList() |
||||
crossAxisSizes += currentCrossAxisSize |
||||
crossAxisPositions += crossAxisSpace |
||||
|
||||
crossAxisSpace += currentCrossAxisSize |
||||
mainAxisSpace = max(mainAxisSpace, currentMainAxisSize) |
||||
|
||||
currentSequence.clear() |
||||
currentMainAxisSize = 0 |
||||
currentCrossAxisSize = 0 |
||||
} |
||||
|
||||
for (measurable in measurables) { |
||||
// Ask the child for its preferred size. |
||||
val placeable = measurable.measure(childConstraints) |
||||
|
||||
// Start a new sequence if there is not enough space. |
||||
if (!canAddToCurrentSequence(placeable)) startNewSequence() |
||||
|
||||
// Add the child to the current sequence. |
||||
if (currentSequence.isNotEmpty()) { |
||||
currentMainAxisSize += mainAxisSpacing.roundToPx() |
||||
} |
||||
currentSequence.add(placeable) |
||||
currentMainAxisSize += placeable.mainAxisSize() |
||||
currentCrossAxisSize = max(currentCrossAxisSize, placeable.crossAxisSize()) |
||||
} |
||||
|
||||
if (currentSequence.isNotEmpty()) startNewSequence() |
||||
|
||||
val mainAxisLayoutSize = if (constraints.mainAxisMax != Constraints.Infinity && |
||||
mainAxisSize == SizeMode.Expand |
||||
) { |
||||
constraints.mainAxisMax |
||||
} else { |
||||
max(mainAxisSpace, constraints.mainAxisMin) |
||||
} |
||||
val crossAxisLayoutSize = max(crossAxisSpace, constraints.crossAxisMin) |
||||
|
||||
val layoutWidth = if (orientation == LayoutOrientation.Horizontal) { |
||||
mainAxisLayoutSize |
||||
} else { |
||||
crossAxisLayoutSize |
||||
} |
||||
val layoutHeight = if (orientation == LayoutOrientation.Horizontal) { |
||||
crossAxisLayoutSize |
||||
} else { |
||||
mainAxisLayoutSize |
||||
} |
||||
|
||||
layout(layoutWidth, layoutHeight) { |
||||
sequences.forEachIndexed { i, placeables -> |
||||
val childrenMainAxisSizes = IntArray(placeables.size) { j -> |
||||
placeables[j].mainAxisSize() + |
||||
if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0 |
||||
} |
||||
val arrangement = if (i < sequences.lastIndex) { |
||||
mainAxisAlignment.arrangement |
||||
} else { |
||||
lastLineMainAxisAlignment.arrangement |
||||
} |
||||
// TODO(soboleva): rtl support |
||||
// Handle vertical direction |
||||
val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 } |
||||
with(arrangement) { |
||||
arrange(mainAxisLayoutSize, childrenMainAxisSizes, mainAxisPositions) |
||||
} |
||||
placeables.forEachIndexed { j, placeable -> |
||||
val crossAxis = when (crossAxisAlignment) { |
||||
FlowCrossAxisAlignment.Start -> 0 |
||||
FlowCrossAxisAlignment.End -> |
||||
crossAxisSizes[i] - placeable.crossAxisSize() |
||||
FlowCrossAxisAlignment.Center -> |
||||
Alignment.Center.align( |
||||
IntSize.Zero, |
||||
IntSize( |
||||
width = 0, |
||||
height = crossAxisSizes[i] - placeable.crossAxisSize() |
||||
), |
||||
LayoutDirection.Ltr |
||||
).y |
||||
} |
||||
if (orientation == LayoutOrientation.Horizontal) { |
||||
placeable.place( |
||||
x = mainAxisPositions[j], |
||||
y = crossAxisPositions[i] + crossAxis |
||||
) |
||||
} else { |
||||
placeable.place( |
||||
x = crossAxisPositions[i] + crossAxis, |
||||
y = mainAxisPositions[j] |
||||
) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Used to specify how a layout chooses its own size when multiple behaviors are possible. |
||||
*/ |
||||
// TODO(popam): remove this when Flow is reworked |
||||
public enum class SizeMode { |
||||
/** |
||||
* Minimize the amount of free space by wrapping the children, |
||||
* subject to the incoming layout constraints. |
||||
*/ |
||||
Wrap, |
||||
/** |
||||
* Maximize the amount of free space by expanding to fill the available space, |
||||
* subject to the incoming layout constraints. |
||||
*/ |
||||
Expand |
||||
} |
||||
|
||||
/** |
||||
* Used to specify the alignment of a layout's children, in main axis direction. |
||||
*/ |
||||
public enum class MainAxisAlignment(internal val arrangement: Arrangement.Vertical) { |
||||
// TODO(soboleva) support RTl in Flow |
||||
// workaround for now - use Arrangement that equals to previous Arrangement |
||||
/** |
||||
* Place children such that they are as close as possible to the middle of the main axis. |
||||
*/ |
||||
Center(Arrangement.Center), |
||||
|
||||
/** |
||||
* Place children such that they are as close as possible to the start of the main axis. |
||||
*/ |
||||
Start(Arrangement.Top), |
||||
|
||||
/** |
||||
* Place children such that they are as close as possible to the end of the main axis. |
||||
*/ |
||||
End(Arrangement.Bottom), |
||||
|
||||
/** |
||||
* Place children such that they are spaced evenly across the main axis, including free |
||||
* space before the first child and after the last child. |
||||
*/ |
||||
SpaceEvenly(Arrangement.SpaceEvenly), |
||||
|
||||
/** |
||||
* Place children such that they are spaced evenly across the main axis, without free |
||||
* space before the first child or after the last child. |
||||
*/ |
||||
SpaceBetween(Arrangement.SpaceBetween), |
||||
|
||||
/** |
||||
* Place children such that they are spaced evenly across the main axis, including free |
||||
* space before the first child and after the last child, but half the amount of space |
||||
* existing otherwise between two consecutive children. |
||||
*/ |
||||
SpaceAround(Arrangement.SpaceAround); |
||||
} |
@ -0,0 +1,68 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.model |
||||
|
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.filled.Star |
||||
import androidx.compose.runtime.Stable |
||||
import androidx.compose.runtime.mutableStateOf |
||||
import androidx.compose.ui.graphics.vector.ImageVector |
||||
import com.example.jetsnack.Android |
||||
import com.example.jetsnack.SortByAlpha |
||||
|
||||
@Stable |
||||
class Filter( |
||||
val name: String, |
||||
enabled: Boolean = false, |
||||
val icon: ImageVector? = null |
||||
) { |
||||
val enabled = mutableStateOf(enabled) |
||||
} |
||||
val filters = listOf( |
||||
Filter(name = "Organic"), |
||||
Filter(name = "Gluten-free"), |
||||
Filter(name = "Dairy-free"), |
||||
Filter(name = "Sweet"), |
||||
Filter(name = "Savory") |
||||
) |
||||
val priceFilters = listOf( |
||||
Filter(name = "$"), |
||||
Filter(name = "$$"), |
||||
Filter(name = "$$$"), |
||||
Filter(name = "$$$$") |
||||
) |
||||
val sortFilters = listOf( |
||||
Filter(name = "Android's favorite (default)", icon = Icons.Filled.Android), |
||||
Filter(name = "Rating", icon = Icons.Filled.Star), |
||||
Filter(name = "Alphabetical", icon = Icons.Filled.SortByAlpha) |
||||
) |
||||
|
||||
val categoryFilters = listOf( |
||||
Filter(name = "Chips & crackers"), |
||||
Filter(name = "Fruit snacks"), |
||||
Filter(name = "Desserts"), |
||||
Filter(name = "Nuts") |
||||
) |
||||
val lifeStyleFilters = listOf( |
||||
Filter(name = "Organic"), |
||||
Filter(name = "Gluten-free"), |
||||
Filter(name = "Dairy-free"), |
||||
Filter(name = "Sweet"), |
||||
Filter(name = "Savory") |
||||
) |
||||
|
||||
var sortDefault = sortFilters.get(0).name |
@ -0,0 +1,137 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.model |
||||
|
||||
import androidx.compose.runtime.Immutable |
||||
import kotlinx.coroutines.Dispatchers |
||||
import kotlinx.coroutines.delay |
||||
import kotlinx.coroutines.withContext |
||||
|
||||
/** |
||||
* A fake repo for searching. |
||||
*/ |
||||
object SearchRepo { |
||||
fun getCategories(): List<SearchCategoryCollection> = searchCategoryCollections |
||||
fun getSuggestions(): List<SearchSuggestionGroup> = searchSuggestions |
||||
|
||||
suspend fun search(query: String): List<Snack> = withContext(Dispatchers.Default) { |
||||
delay(200L) // simulate an I/O delay |
||||
snacks.filter { it.name.contains(query, ignoreCase = true) } |
||||
} |
||||
} |
||||
|
||||
@Immutable |
||||
data class SearchCategoryCollection( |
||||
val id: Long, |
||||
val name: String, |
||||
val categories: List<SearchCategory> |
||||
) |
||||
|
||||
@Immutable |
||||
data class SearchCategory( |
||||
val name: String, |
||||
val imageUrl: String |
||||
) |
||||
|
||||
@Immutable |
||||
data class SearchSuggestionGroup( |
||||
val id: Long, |
||||
val name: String, |
||||
val suggestions: List<String> |
||||
) |
||||
|
||||
/** |
||||
* Static data |
||||
*/ |
||||
|
||||
private val searchCategoryCollections = listOf( |
||||
SearchCategoryCollection( |
||||
id = 0L, |
||||
name = "Categories", |
||||
categories = listOf( |
||||
SearchCategory( |
||||
name = "Chips & crackers", |
||||
imageUrl = "https://source.unsplash.com/UsSdMZ78Q3E" |
||||
), |
||||
SearchCategory( |
||||
name = "Fruit snacks", |
||||
imageUrl = "https://source.unsplash.com/SfP1PtM9Qa8" |
||||
), |
||||
SearchCategory( |
||||
name = "Desserts", |
||||
imageUrl = "https://source.unsplash.com/_jk8KIyN_uA" |
||||
), |
||||
SearchCategory( |
||||
name = "Nuts ", |
||||
imageUrl = "https://source.unsplash.com/UsSdMZ78Q3E" |
||||
) |
||||
) |
||||
), |
||||
SearchCategoryCollection( |
||||
id = 1L, |
||||
name = "Lifestyles", |
||||
categories = listOf( |
||||
SearchCategory( |
||||
name = "Organic", |
||||
imageUrl = "https://source.unsplash.com/7meCnGCJ5Ms" |
||||
), |
||||
SearchCategory( |
||||
name = "Gluten Free", |
||||
imageUrl = "https://source.unsplash.com/m741tj4Cz7M" |
||||
), |
||||
SearchCategory( |
||||
name = "Paleo", |
||||
imageUrl = "https://source.unsplash.com/dt5-8tThZKg" |
||||
), |
||||
SearchCategory( |
||||
name = "Vegan", |
||||
imageUrl = "https://source.unsplash.com/ReXxkS1m1H0" |
||||
), |
||||
SearchCategory( |
||||
name = "Vegitarian", |
||||
imageUrl = "https://source.unsplash.com/IGfIGP5ONV0" |
||||
), |
||||
SearchCategory( |
||||
name = "Whole30", |
||||
imageUrl = "https://source.unsplash.com/9MzCd76xLGk" |
||||
) |
||||
) |
||||
) |
||||
) |
||||
|
||||
private val searchSuggestions = listOf( |
||||
SearchSuggestionGroup( |
||||
id = 0L, |
||||
name = "Recent searches", |
||||
suggestions = listOf( |
||||
"Cheese", |
||||
"Apple Sauce" |
||||
) |
||||
), |
||||
SearchSuggestionGroup( |
||||
id = 1L, |
||||
name = "Popular searches", |
||||
suggestions = listOf( |
||||
"Organic", |
||||
"Gluten Free", |
||||
"Paleo", |
||||
"Vegan", |
||||
"Vegitarian", |
||||
"Whole30" |
||||
) |
||||
) |
||||
) |
@ -0,0 +1,226 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.model |
||||
|
||||
import androidx.compose.runtime.Immutable |
||||
|
||||
@Immutable |
||||
data class Snack( |
||||
val id: Long, |
||||
val name: String, |
||||
val imageUrl: String, |
||||
val price: Long, |
||||
val tagline: String = "", |
||||
val tags: Set<String> = emptySet() |
||||
) |
||||
|
||||
/** |
||||
* Static data |
||||
*/ |
||||
|
||||
val snacks = listOf( |
||||
Snack( |
||||
id = 1L, |
||||
name = "Cupcake", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/pGM4sjt_BdQ", |
||||
price = 299 |
||||
), |
||||
Snack( |
||||
id = 2L, |
||||
name = "Donut", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/Yc5sL-ejk6U", |
||||
price = 290 |
||||
), |
||||
Snack( |
||||
id = 3L, |
||||
name = "Eclair", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/-LojFX9NfPY", |
||||
price = 289 |
||||
), |
||||
Snack( |
||||
id = 4L, |
||||
name = "Froyo", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/3U2V5WqK1PQ", |
||||
price = 288 |
||||
), |
||||
Snack( |
||||
id = 5L, |
||||
name = "Gingerbread", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/Y4YR9OjdIMk", |
||||
price = 499 |
||||
), |
||||
Snack( |
||||
id = 6L, |
||||
name = "Honeycomb", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/bELvIg_KZGU", |
||||
price = 309 |
||||
), |
||||
Snack( |
||||
id = 7L, |
||||
name = "Ice Cream Sandwich", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/YgYJsFDd4AU", |
||||
price = 1299 |
||||
), |
||||
Snack( |
||||
id = 8L, |
||||
name = "Jellybean", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/0u_vbeOkMpk", |
||||
price = 109 |
||||
), |
||||
Snack( |
||||
id = 9L, |
||||
name = "KitKat", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/yb16pT5F_jE", |
||||
price = 549 |
||||
), |
||||
Snack( |
||||
id = 10L, |
||||
name = "Lollipop", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/AHF_ZktTL6Q", |
||||
price = 209 |
||||
), |
||||
Snack( |
||||
id = 11L, |
||||
name = "Marshmallow", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/rqFm0IgMVYY", |
||||
price = 219 |
||||
), |
||||
Snack( |
||||
id = 12L, |
||||
name = "Nougat", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/qRE_OpbVPR8", |
||||
price = 309 |
||||
), |
||||
Snack( |
||||
id = 13L, |
||||
name = "Oreo", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/33fWPnyN6tU", |
||||
price = 339 |
||||
), |
||||
Snack( |
||||
id = 14L, |
||||
name = "Pie", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/aX_ljOOyWJY", |
||||
price = 249 |
||||
), |
||||
Snack( |
||||
id = 15L, |
||||
name = "Chips", |
||||
imageUrl = "https://source.unsplash.com/UsSdMZ78Q3E", |
||||
price = 277 |
||||
), |
||||
Snack( |
||||
id = 16L, |
||||
name = "Pretzels", |
||||
imageUrl = "https://source.unsplash.com/7meCnGCJ5Ms", |
||||
price = 154 |
||||
), |
||||
Snack( |
||||
id = 17L, |
||||
name = "Smoothies", |
||||
imageUrl = "https://source.unsplash.com/m741tj4Cz7M", |
||||
price = 257 |
||||
), |
||||
Snack( |
||||
id = 18L, |
||||
name = "Popcorn", |
||||
imageUrl = "https://source.unsplash.com/iuwMdNq0-s4", |
||||
price = 167 |
||||
), |
||||
Snack( |
||||
id = 19L, |
||||
name = "Almonds", |
||||
imageUrl = "https://source.unsplash.com/qgWWQU1SzqM", |
||||
price = 123 |
||||
), |
||||
Snack( |
||||
id = 20L, |
||||
name = "Cheese", |
||||
imageUrl = "https://source.unsplash.com/9MzCd76xLGk", |
||||
price = 231 |
||||
), |
||||
Snack( |
||||
id = 21L, |
||||
name = "Apples", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/1d9xXWMtQzQ", |
||||
price = 221 |
||||
), |
||||
Snack( |
||||
id = 22L, |
||||
name = "Apple sauce", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/wZxpOw84QTU", |
||||
price = 222 |
||||
), |
||||
Snack( |
||||
id = 23L, |
||||
name = "Apple chips", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/okzeRxm_GPo", |
||||
price = 231 |
||||
), |
||||
Snack( |
||||
id = 24L, |
||||
name = "Apple juice", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/l7imGdupuhU", |
||||
price = 241 |
||||
), |
||||
Snack( |
||||
id = 25L, |
||||
name = "Apple pie", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/bkXzABDt08Q", |
||||
price = 225 |
||||
), |
||||
Snack( |
||||
id = 26L, |
||||
name = "Grapes", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/y2MeW00BdBo", |
||||
price = 266 |
||||
), |
||||
Snack( |
||||
id = 27L, |
||||
name = "Kiwi", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/1oMGgHn-M8k", |
||||
price = 127 |
||||
), |
||||
Snack( |
||||
id = 28L, |
||||
name = "Mango", |
||||
tagline = "A tag line", |
||||
imageUrl = "https://source.unsplash.com/TIGDsyy0TK4", |
||||
price = 128 |
||||
) |
||||
) |
@ -0,0 +1,113 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.model |
||||
|
||||
import androidx.compose.runtime.Immutable |
||||
|
||||
@Immutable |
||||
data class SnackCollection( |
||||
val id: Long, |
||||
val name: String, |
||||
val snacks: List<Snack>, |
||||
val type: CollectionType = CollectionType.Normal |
||||
) |
||||
|
||||
enum class CollectionType { Normal, Highlight } |
||||
|
||||
/** |
||||
* A fake repo |
||||
*/ |
||||
object SnackRepo { |
||||
fun getSnacks(): List<SnackCollection> = snackCollections |
||||
fun getSnack(snackId: Long) = snacks.find { it.id == snackId }!! |
||||
fun getRelated(@Suppress("UNUSED_PARAMETER") snackId: Long) = related |
||||
fun getInspiredByCart() = inspiredByCart |
||||
fun getFilters() = filters |
||||
fun getPriceFilters() = priceFilters |
||||
fun getCart() = cart |
||||
fun getSortFilters() = sortFilters |
||||
fun getCategoryFilters() = categoryFilters |
||||
fun getSortDefault() = sortDefault |
||||
fun getLifeStyleFilters() = lifeStyleFilters |
||||
} |
||||
|
||||
/** |
||||
* Static data |
||||
*/ |
||||
|
||||
private val tastyTreats = SnackCollection( |
||||
id = 1L, |
||||
name = "Android's picks", |
||||
type = CollectionType.Highlight, |
||||
snacks = snacks.subList(0, 13) |
||||
) |
||||
|
||||
private val popular = SnackCollection( |
||||
id = 2L, |
||||
name = "Popular on Jetsnack", |
||||
snacks = snacks.subList(14, 19) |
||||
) |
||||
|
||||
private val wfhFavs = tastyTreats.copy( |
||||
id = 3L, |
||||
name = "WFH favourites" |
||||
) |
||||
|
||||
private val newlyAdded = popular.copy( |
||||
id = 4L, |
||||
name = "Newly Added" |
||||
) |
||||
|
||||
private val exclusive = tastyTreats.copy( |
||||
id = 5L, |
||||
name = "Only on Jetsnack" |
||||
) |
||||
|
||||
private val also = tastyTreats.copy( |
||||
id = 6L, |
||||
name = "Customers also bought" |
||||
) |
||||
|
||||
private val inspiredByCart = tastyTreats.copy( |
||||
id = 7L, |
||||
name = "Inspired by your cart" |
||||
) |
||||
|
||||
private val snackCollections = listOf( |
||||
tastyTreats, |
||||
popular, |
||||
wfhFavs, |
||||
newlyAdded, |
||||
exclusive |
||||
) |
||||
|
||||
private val related = listOf( |
||||
also, |
||||
popular |
||||
) |
||||
|
||||
private val cart = listOf( |
||||
OrderLine(snacks[4], 2), |
||||
OrderLine(snacks[6], 3), |
||||
OrderLine(snacks[8], 1) |
||||
) |
||||
|
||||
@Immutable |
||||
data class OrderLine( |
||||
val snack: Snack, |
||||
val count: Int |
||||
) |
@ -0,0 +1,50 @@
|
||||
/* |
||||
* Copyright 2021 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.model |
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow |
||||
import kotlinx.coroutines.flow.StateFlow |
||||
import kotlinx.coroutines.flow.asStateFlow |
||||
import kotlinx.coroutines.flow.update |
||||
|
||||
data class Message(val id: Long, val message: Int /*@StringRes*/) |
||||
|
||||
/** |
||||
* Class responsible for managing Snackbar messages to show on the screen |
||||
*/ |
||||
object SnackbarManager { |
||||
|
||||
private val _messages: MutableStateFlow<List<Message>> = MutableStateFlow(emptyList()) |
||||
val messages: StateFlow<List<Message>> get() = _messages.asStateFlow() |
||||
|
||||
fun showMessage(message: Int /*@StringRes*/) { |
||||
_messages.update { currentMessages -> |
||||
currentMessages + Message( |
||||
id = createRandomUUID(), |
||||
message = message |
||||
) |
||||
} |
||||
} |
||||
|
||||
fun setMessageShown(messageId: Long) { |
||||
_messages.update { currentMessages -> |
||||
currentMessages.filterNot { it.id == messageId } |
||||
} |
||||
} |
||||
} |
||||
|
||||
expect fun createRandomUUID(): Long |
@ -0,0 +1,59 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui |
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues |
||||
import androidx.compose.material.SnackbarHost |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import com.example.jetsnack.ui.components.JetsnackScaffold |
||||
import com.example.jetsnack.ui.components.JetsnackSnackbar |
||||
import com.example.jetsnack.ui.home.JetsnackBottomBar |
||||
import com.example.jetsnack.ui.snackdetail.jetSnackSystemBarsPadding |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
@Composable |
||||
fun JetsnackApp() { |
||||
JetsnackTheme { |
||||
val appState = rememberMppJetsnackAppState() |
||||
JetsnackScaffold( |
||||
bottomBar = { |
||||
if (appState.shouldShowBottomBar()) { |
||||
JetsnackBottomBar( |
||||
tabs = appState.bottomBarTabs, |
||||
currentRoute = appState.currentRoute!!, |
||||
navigateToRoute = appState::navigateToBottomBarRoute |
||||
) |
||||
} |
||||
}, |
||||
snackbarHost = { |
||||
SnackbarHost( |
||||
hostState = it, |
||||
modifier = Modifier.jetSnackSystemBarsPadding(), |
||||
snackbar = { snackbarData -> JetsnackSnackbar(snackbarData) } |
||||
) |
||||
}, |
||||
scaffoldState = appState.scaffoldState |
||||
) { innerPaddingModifier -> |
||||
JetsnackScaffoldContent(innerPaddingModifier, appState) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
expect fun JetsnackScaffoldContent(innerPaddingModifier: PaddingValues, appState: MppJetsnackAppState) |
||||
|
@ -0,0 +1,166 @@
|
||||
/* |
||||
* Copyright 2021 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui |
||||
|
||||
import androidx.compose.material.ScaffoldState |
||||
import androidx.compose.material.rememberScaffoldState |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.ReadOnlyComposable |
||||
import androidx.compose.runtime.Stable |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.rememberCoroutineScope |
||||
import com.example.jetsnack.model.SnackbarManager |
||||
import com.example.jetsnack.ui.home.HomeSections |
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.flow.collect |
||||
import kotlinx.coroutines.launch |
||||
|
||||
/** |
||||
* Destinations used in the [JetsnackApp]. |
||||
*/ |
||||
object MainDestinations { |
||||
const val HOME_ROUTE = "home" |
||||
const val SNACK_DETAIL_ROUTE = "snack" |
||||
const val SNACK_ID_KEY = "snackId" |
||||
} |
||||
|
||||
|
||||
@Composable |
||||
expect fun rememberMppJetsnackAppState(): MppJetsnackAppState |
||||
|
||||
@Stable |
||||
expect class MppJetsnackAppState { |
||||
|
||||
val scaffoldState: ScaffoldState |
||||
val snackbarManager: SnackbarManager |
||||
val coroutineScope: CoroutineScope |
||||
val bottomBarTabs: Array<HomeSections> |
||||
val currentRoute: String? |
||||
|
||||
@Composable |
||||
fun shouldShowBottomBar(): Boolean |
||||
|
||||
fun navigateToBottomBarRoute(route: String) |
||||
} |
||||
|
||||
/** |
||||
* Responsible for holding state related to [JetsnackApp] and containing UI-related logic. |
||||
*/ |
||||
@Stable |
||||
class JetsnackAppState( |
||||
val scaffoldState: ScaffoldState, |
||||
// val navController: NavHostController, |
||||
private val snackbarManager: SnackbarManager, |
||||
// private val resources: Resources, |
||||
coroutineScope: CoroutineScope |
||||
) { |
||||
// Process snackbars coming from SnackbarManager |
||||
init { |
||||
coroutineScope.launch { |
||||
snackbarManager.messages.collect { currentMessages -> |
||||
if (currentMessages.isNotEmpty()) { |
||||
val message = currentMessages[0] |
||||
// TODO: implement |
||||
val text = "TODO: resources.getText(message.messageId)" |
||||
|
||||
// Display the snackbar on the screen. `showSnackbar` is a function |
||||
// that suspends until the snackbar disappears from the screen |
||||
scaffoldState.snackbarHostState.showSnackbar(text.toString()) |
||||
// Once the snackbar is gone or dismissed, notify the SnackbarManager |
||||
snackbarManager.setMessageShown(message.id) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// ---------------------------------------------------------- |
||||
// BottomBar state source of truth |
||||
// ---------------------------------------------------------- |
||||
|
||||
val bottomBarTabs = HomeSections.values() |
||||
private val bottomBarRoutes = bottomBarTabs.map { it.route } |
||||
|
||||
// Reading this attribute will cause recompositions when the bottom bar needs shown, or not. |
||||
// Not all routes need to show the bottom bar. |
||||
val shouldShowBottomBar: Boolean |
||||
@Composable get() = true |
||||
// navController |
||||
// .currentBackStackEntryAsState().value?.destination?.route in bottomBarRoutes |
||||
|
||||
// ---------------------------------------------------------- |
||||
// Navigation state source of truth |
||||
// ---------------------------------------------------------- |
||||
|
||||
val currentRoute: String? |
||||
get() = HomeSections.FEED.route//navController.currentDestination?.route |
||||
|
||||
fun upPress() { |
||||
// navController.navigateUp() |
||||
} |
||||
|
||||
fun navigateToBottomBarRoute(route: String) { |
||||
// if (route != currentRoute) { |
||||
// navController.navigate(route) { |
||||
// launchSingleTop = true |
||||
// restoreState = true |
||||
// // Pop up backstack to the first destination and save state. This makes going back |
||||
// // to the start destination when pressing back in any other bottom tab. |
||||
// popUpTo(findStartDestination(navController.graph).id) { |
||||
// saveState = true |
||||
// } |
||||
// } |
||||
// } |
||||
} |
||||
|
||||
// fun navigateToSnackDetail(snackId: Long, from: NavBackStackEntry) { |
||||
// In order to discard duplicated navigation events, we check the Lifecycle |
||||
// if (from.lifecycleIsResumed()) { |
||||
// navController.navigate("${MainDestinations.SNACK_DETAIL_ROUTE}/$snackId") |
||||
// } |
||||
// } |
||||
} |
||||
|
||||
/** |
||||
* If the lifecycle is not resumed it means this NavBackStackEntry already processed a nav event. |
||||
* |
||||
* This is used to de-duplicate navigation events. |
||||
*/ |
||||
//private fun NavBackStackEntry.lifecycleIsResumed() = |
||||
// this.getLifecycle().currentState == Lifecycle.State.RESUMED |
||||
// |
||||
//private val NavGraph.startDestination: NavDestination? |
||||
// get() = findNode(startDestinationId) |
||||
|
||||
/** |
||||
* Copied from similar function in NavigationUI.kt |
||||
* |
||||
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt |
||||
*/ |
||||
//private tailrec fun findStartDestination(graph: NavDestination): NavDestination { |
||||
// return if (graph is NavGraph) findStartDestination(graph.startDestination!!) else graph |
||||
//} |
||||
|
||||
/** |
||||
* A composable function that returns the [Resources]. It will be recomposed when `Configuration` |
||||
* gets updated. |
||||
*/ |
||||
//@Composable |
||||
//@ReadOnlyComposable |
||||
//private fun resources(): Resources { |
||||
// LocalConfiguration.current |
||||
// return LocalContext.current.resources |
||||
//} |
@ -0,0 +1,126 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
import androidx.compose.foundation.BorderStroke |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.indication |
||||
import androidx.compose.foundation.interaction.MutableInteractionSource |
||||
import androidx.compose.foundation.layout.Arrangement |
||||
import androidx.compose.foundation.layout.PaddingValues |
||||
import androidx.compose.foundation.layout.Row |
||||
import androidx.compose.foundation.layout.RowScope |
||||
import androidx.compose.foundation.layout.defaultMinSize |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
import androidx.compose.material.ButtonDefaults |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.ProvideTextStyle |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.material.ripple.rememberRipple |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.remember |
||||
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.RectangleShape |
||||
import androidx.compose.ui.graphics.Shape |
||||
import androidx.compose.ui.semantics.Role |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
@Composable |
||||
|
||||
fun JetsnackButton( |
||||
onClick: () -> Unit, |
||||
modifier: Modifier = Modifier, |
||||
enabled: Boolean = true, |
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, |
||||
shape: Shape = ButtonShape, |
||||
border: BorderStroke? = null, |
||||
backgroundGradient: List<Color> = JetsnackTheme.colors.interactivePrimary, |
||||
disabledBackgroundGradient: List<Color> = JetsnackTheme.colors.interactiveSecondary, |
||||
contentColor: Color = JetsnackTheme.colors.textInteractive, |
||||
disabledContentColor: Color = JetsnackTheme.colors.textHelp, |
||||
contentPadding: PaddingValues = ButtonDefaults.ContentPadding, |
||||
content: @Composable RowScope.() -> Unit |
||||
) { |
||||
JetsnackSurface( |
||||
shape = shape, |
||||
color = Color.Transparent, |
||||
contentColor = if (enabled) contentColor else disabledContentColor, |
||||
border = border, |
||||
modifier = modifier |
||||
.clip(shape) |
||||
.background( |
||||
Brush.horizontalGradient( |
||||
colors = if (enabled) backgroundGradient else disabledBackgroundGradient |
||||
) |
||||
) |
||||
.clickable( |
||||
onClick = onClick, |
||||
enabled = enabled, |
||||
role = Role.Button, |
||||
interactionSource = interactionSource, |
||||
indication = null |
||||
) |
||||
) { |
||||
ProvideTextStyle( |
||||
value = MaterialTheme.typography.button |
||||
) { |
||||
Row( |
||||
Modifier |
||||
.defaultMinSize( |
||||
minWidth = ButtonDefaults.MinWidth, |
||||
minHeight = ButtonDefaults.MinHeight |
||||
) |
||||
.indication(interactionSource, rememberRipple()) |
||||
.padding(contentPadding), |
||||
horizontalArrangement = Arrangement.Center, |
||||
verticalAlignment = Alignment.CenterVertically, |
||||
content = content |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private val ButtonShape = RoundedCornerShape(percent = 50) |
||||
|
||||
//@Preview |
||||
@Composable |
||||
private fun ButtonPreview() { |
||||
JetsnackTheme { |
||||
JetsnackButton(onClick = {}) { |
||||
Text(text = "Demo") |
||||
} |
||||
} |
||||
} |
||||
|
||||
//@Preview |
||||
@Composable |
||||
private fun RectangleButtonPreview() { |
||||
JetsnackTheme { |
||||
JetsnackButton( |
||||
onClick = {}, shape = RectangleShape |
||||
) { |
||||
Text(text = "Demo") |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,61 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
import androidx.compose.foundation.BorderStroke |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.graphics.Shape |
||||
import androidx.compose.ui.unit.Dp |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
@Composable |
||||
fun JetsnackCard( |
||||
modifier: Modifier = Modifier, |
||||
shape: Shape = MaterialTheme.shapes.medium, |
||||
color: Color = JetsnackTheme.colors.uiBackground, |
||||
contentColor: Color = JetsnackTheme.colors.textPrimary, |
||||
border: BorderStroke? = null, |
||||
elevation: Dp = 4.dp, |
||||
content: @Composable () -> Unit |
||||
) { |
||||
JetsnackSurface( |
||||
modifier = modifier, |
||||
shape = shape, |
||||
color = color, |
||||
contentColor = contentColor, |
||||
elevation = elevation, |
||||
border = border, |
||||
content = content |
||||
) |
||||
} |
||||
|
||||
//@Preview |
||||
@Composable |
||||
private fun CardPreview() { |
||||
JetsnackTheme { |
||||
JetsnackCard { |
||||
Text(text = "Demo", modifier = Modifier.padding(16.dp)) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,56 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.size |
||||
import androidx.compose.material.Divider |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.unit.Dp |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
@Composable |
||||
fun JetsnackDivider( |
||||
modifier: Modifier = Modifier, |
||||
color: Color = JetsnackTheme.colors.uiBorder.copy(alpha = DividerAlpha), |
||||
thickness: Dp = 1.dp, |
||||
startIndent: Dp = 0.dp |
||||
) { |
||||
Divider( |
||||
modifier = modifier, |
||||
color = color, |
||||
thickness = thickness, |
||||
startIndent = startIndent |
||||
) |
||||
} |
||||
|
||||
private const val DividerAlpha = 0.12f |
||||
|
||||
//@Preview |
||||
@Composable |
||||
private fun DividerPreview() { |
||||
JetsnackTheme { |
||||
Box(Modifier.size(height = 10.dp, width = 100.dp)) { |
||||
JetsnackDivider(Modifier.align(Alignment.Center)) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,162 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
import androidx.compose.animation.animateColorAsState |
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.interaction.MutableInteractionSource |
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState |
||||
import androidx.compose.foundation.layout.Arrangement |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.PaddingValues |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.heightIn |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.lazy.LazyRow |
||||
import androidx.compose.foundation.lazy.items |
||||
import androidx.compose.foundation.selection.toggleable |
||||
import androidx.compose.foundation.shape.CircleShape |
||||
import androidx.compose.material.Icon |
||||
import androidx.compose.material.IconButton |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.graphics.Shape |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.FilterList |
||||
import com.example.jetsnack.MppR |
||||
import com.example.jetsnack.label_filters |
||||
import com.example.jetsnack.model.Filter |
||||
import com.example.jetsnack.stringResource |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
@Composable |
||||
fun FilterBar( |
||||
filters: List<Filter>, |
||||
onShowFilters: () -> Unit |
||||
) { |
||||
|
||||
LazyRow( |
||||
verticalAlignment = Alignment.CenterVertically, |
||||
horizontalArrangement = Arrangement.spacedBy(8.dp), |
||||
contentPadding = PaddingValues(start = 12.dp, end = 8.dp), |
||||
modifier = Modifier.heightIn(min = 56.dp) |
||||
) { |
||||
item { |
||||
IconButton(onClick = onShowFilters) { |
||||
Icon( |
||||
imageVector = Icons.Rounded.FilterList, |
||||
tint = JetsnackTheme.colors.brand, |
||||
contentDescription = stringResource(MppR.string.label_filters), |
||||
modifier = Modifier.diagonalGradientBorder( |
||||
colors = JetsnackTheme.colors.interactiveSecondary, |
||||
shape = CircleShape |
||||
) |
||||
) |
||||
} |
||||
} |
||||
items(filters) { filter -> |
||||
FilterChip(filter = filter, shape = MaterialTheme.shapes.small) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun FilterChip( |
||||
filter: Filter, |
||||
modifier: Modifier = Modifier, |
||||
shape: Shape = MaterialTheme.shapes.small |
||||
) { |
||||
val (selected, setSelected) = filter.enabled |
||||
val backgroundColor by animateColorAsState( |
||||
if (selected) JetsnackTheme.colors.brandSecondary else JetsnackTheme.colors.uiBackground |
||||
) |
||||
val border = Modifier.fadeInDiagonalGradientBorder( |
||||
showBorder = !selected, |
||||
colors = JetsnackTheme.colors.interactiveSecondary, |
||||
shape = shape |
||||
) |
||||
val textColor by animateColorAsState( |
||||
if (selected) Color.Black else JetsnackTheme.colors.textSecondary |
||||
) |
||||
|
||||
JetsnackSurface( |
||||
modifier = modifier.height(28.dp), |
||||
color = backgroundColor, |
||||
contentColor = textColor, |
||||
shape = shape, |
||||
elevation = 2.dp |
||||
) { |
||||
val interactionSource = remember { MutableInteractionSource() } |
||||
|
||||
val pressed by interactionSource.collectIsPressedAsState() |
||||
val backgroundPressed = |
||||
if (pressed) { |
||||
Modifier.offsetGradientBackground( |
||||
JetsnackTheme.colors.interactiveSecondary, |
||||
200f, |
||||
0f |
||||
) |
||||
} else { |
||||
Modifier.background(Color.Transparent) |
||||
} |
||||
Box( |
||||
modifier = Modifier |
||||
.toggleable( |
||||
value = selected, |
||||
onValueChange = setSelected, |
||||
interactionSource = interactionSource, |
||||
indication = null |
||||
) |
||||
.then(backgroundPressed) |
||||
.then(border), |
||||
) { |
||||
Text( |
||||
text = filter.name, |
||||
style = MaterialTheme.typography.caption, |
||||
maxLines = 1, |
||||
modifier = Modifier.padding( |
||||
horizontal = 20.dp, |
||||
vertical = 6.dp |
||||
) |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
//@Preview |
||||
@Composable |
||||
private fun FilterDisabledPreview() { |
||||
JetsnackTheme { |
||||
FilterChip(Filter(name = "Demo", enabled = false), Modifier.padding(4.dp)) |
||||
} |
||||
} |
||||
|
||||
//@Preview |
||||
@Composable |
||||
private fun FilterEnabledPreview() { |
||||
JetsnackTheme { |
||||
FilterChip(Filter(name = "Demo", enabled = true)) |
||||
} |
||||
} |
@ -0,0 +1,81 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
import androidx.compose.animation.animateColorAsState |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.border |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.composed |
||||
import androidx.compose.ui.draw.drawWithContent |
||||
import androidx.compose.ui.graphics.BlendMode |
||||
import androidx.compose.ui.graphics.Brush |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.graphics.Shape |
||||
import androidx.compose.ui.graphics.TileMode |
||||
import androidx.compose.ui.unit.Dp |
||||
import androidx.compose.ui.unit.dp |
||||
|
||||
fun Modifier.diagonalGradientTint( |
||||
colors: List<Color>, |
||||
blendMode: BlendMode |
||||
) = drawWithContent { |
||||
drawContent() |
||||
drawRect( |
||||
brush = Brush.linearGradient(colors), |
||||
blendMode = blendMode |
||||
) |
||||
} |
||||
|
||||
fun Modifier.offsetGradientBackground( |
||||
colors: List<Color>, |
||||
width: Float, |
||||
offset: Float = 0f |
||||
) = background( |
||||
Brush.horizontalGradient( |
||||
colors, |
||||
startX = -offset, |
||||
endX = width - offset, |
||||
tileMode = TileMode.Mirror |
||||
) |
||||
) |
||||
|
||||
fun Modifier.diagonalGradientBorder( |
||||
colors: List<Color>, |
||||
borderSize: Dp = 2.dp, |
||||
shape: Shape |
||||
) = border( |
||||
width = borderSize, |
||||
brush = Brush.linearGradient(colors), |
||||
shape = shape |
||||
) |
||||
|
||||
fun Modifier.fadeInDiagonalGradientBorder( |
||||
showBorder: Boolean, |
||||
colors: List<Color>, |
||||
borderSize: Dp = 2.dp, |
||||
shape: Shape |
||||
) = composed { |
||||
val animatedColors = List(colors.size) { i -> |
||||
animateColorAsState(if (showBorder) colors[i] else colors[i].copy(alpha = 0f)).value |
||||
} |
||||
diagonalGradientBorder( |
||||
colors = animatedColors, |
||||
borderSize = borderSize, |
||||
shape = shape |
||||
) |
||||
} |
@ -0,0 +1,109 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.interaction.MutableInteractionSource |
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.shape.CircleShape |
||||
import androidx.compose.material.Icon |
||||
import androidx.compose.material.Surface |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.filled.Add |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.draw.clip |
||||
import androidx.compose.ui.graphics.BlendMode |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.graphics.vector.ImageVector |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
@Composable |
||||
fun JetsnackGradientTintedIconButton( |
||||
imageVector: ImageVector, |
||||
onClick: () -> Unit, |
||||
contentDescription: String?, |
||||
modifier: Modifier = Modifier, |
||||
colors: List<Color> = JetsnackTheme.colors.interactiveSecondary |
||||
) { |
||||
val interactionSource = remember { MutableInteractionSource() } |
||||
|
||||
// This should use a layer + srcIn but needs investigation |
||||
val border = Modifier.fadeInDiagonalGradientBorder( |
||||
showBorder = true, |
||||
colors = JetsnackTheme.colors.interactiveSecondary, |
||||
shape = CircleShape |
||||
) |
||||
val pressed by interactionSource.collectIsPressedAsState() |
||||
val background = if (pressed) { |
||||
Modifier.offsetGradientBackground(colors, 200f, 0f) |
||||
} else { |
||||
Modifier.background(JetsnackTheme.colors.uiBackground) |
||||
} |
||||
val blendMode = if (JetsnackTheme.colors.isDark) BlendMode.Darken else BlendMode.Plus |
||||
val modifierColor = if (pressed) { |
||||
Modifier.diagonalGradientTint( |
||||
colors = listOf( |
||||
JetsnackTheme.colors.textSecondary, |
||||
JetsnackTheme.colors.textSecondary |
||||
), |
||||
blendMode = blendMode |
||||
) |
||||
} else { |
||||
Modifier.diagonalGradientTint( |
||||
colors = colors, |
||||
blendMode = blendMode |
||||
) |
||||
} |
||||
Surface( |
||||
modifier = modifier |
||||
.clickable( |
||||
onClick = onClick, |
||||
interactionSource = interactionSource, |
||||
indication = null |
||||
) |
||||
.clip(CircleShape) |
||||
.then(border) |
||||
.then(background), |
||||
color = Color.Transparent |
||||
) { |
||||
Icon( |
||||
imageVector = imageVector, |
||||
contentDescription = contentDescription, |
||||
modifier = modifierColor |
||||
) |
||||
} |
||||
} |
||||
|
||||
//@Preview |
||||
@Composable |
||||
private fun GradientTintedIconButtonPreview() { |
||||
JetsnackTheme { |
||||
JetsnackGradientTintedIconButton( |
||||
imageVector = Icons.Default.Add, |
||||
onClick = {}, |
||||
contentDescription = "Demo", |
||||
modifier = Modifier.padding(4.dp) |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,68 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.layout.Layout |
||||
|
||||
/** |
||||
* A simple grid which lays elements out vertically in evenly sized [columns]. |
||||
*/ |
||||
@Composable |
||||
fun VerticalGrid( |
||||
modifier: Modifier = Modifier, |
||||
columns: Int = 2, |
||||
content: @Composable () -> Unit |
||||
) { |
||||
Layout( |
||||
content = content, |
||||
modifier = modifier |
||||
) { measurables, constraints -> |
||||
val itemWidth = constraints.maxWidth / columns |
||||
// Keep given height constraints, but set an exact width |
||||
val itemConstraints = constraints.copy( |
||||
minWidth = itemWidth, |
||||
maxWidth = itemWidth |
||||
) |
||||
// Measure each item with these constraints |
||||
val placeables = measurables.map { it.measure(itemConstraints) } |
||||
// Track each columns height so we can calculate the overall height |
||||
val columnHeights = Array(columns) { 0 } |
||||
placeables.forEachIndexed { index, placeable -> |
||||
val column = index % columns |
||||
columnHeights[column] += placeable.height |
||||
} |
||||
val height = (columnHeights.maxOrNull() ?: constraints.minHeight) |
||||
.coerceAtMost(constraints.maxHeight) |
||||
layout( |
||||
width = constraints.maxWidth, |
||||
height = height |
||||
) { |
||||
// Track the Y co-ord per column we have placed up to |
||||
val columnY = Array(columns) { 0 } |
||||
placeables.forEachIndexed { index, placeable -> |
||||
val column = index % columns |
||||
placeable.placeRelative( |
||||
x = column * itemWidth, |
||||
y = columnY[column] |
||||
) |
||||
columnY[column] += placeable.height |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,109 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
import androidx.compose.animation.Crossfade |
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
import androidx.compose.foundation.layout.Row |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.widthIn |
||||
import androidx.compose.material.ContentAlpha |
||||
import androidx.compose.material.LocalContentAlpha |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.filled.Add |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.CompositionLocalProvider |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.platform.LocalLayoutDirection |
||||
import androidx.compose.ui.text.style.TextAlign |
||||
import androidx.compose.ui.unit.LayoutDirection |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.compose.ui.unit.sp |
||||
import com.example.jetsnack.* |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
@Composable |
||||
fun QuantitySelector( |
||||
count: Int, |
||||
decreaseItemCount: () -> Unit, |
||||
increaseItemCount: () -> Unit, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
Row(modifier = modifier) { |
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { |
||||
Text( |
||||
text = stringResource(MppR.string.quantity), |
||||
style = MaterialTheme.typography.subtitle1, |
||||
color = JetsnackTheme.colors.textSecondary, |
||||
modifier = Modifier |
||||
.padding(end = 18.dp) |
||||
.align(Alignment.CenterVertically) |
||||
) |
||||
} |
||||
JetsnackGradientTintedIconButton( |
||||
imageVector = Icons.Default.Remove, |
||||
onClick = decreaseItemCount, |
||||
contentDescription = stringResource(MppR.string.label_decrease), |
||||
modifier = Modifier.align(Alignment.CenterVertically) |
||||
) |
||||
Crossfade( |
||||
targetState = count, |
||||
modifier = Modifier |
||||
.align(Alignment.CenterVertically) |
||||
) { |
||||
Text( |
||||
text = "$it", |
||||
style = MaterialTheme.typography.subtitle2, |
||||
fontSize = 18.sp, |
||||
color = JetsnackTheme.colors.textPrimary, |
||||
textAlign = TextAlign.Center, |
||||
modifier = Modifier.widthIn(min = 24.dp) |
||||
) |
||||
} |
||||
JetsnackGradientTintedIconButton( |
||||
imageVector = Icons.Default.Add, |
||||
onClick = increaseItemCount, |
||||
contentDescription = stringResource(MppR.string.label_increase), |
||||
modifier = Modifier.align(Alignment.CenterVertically) |
||||
) |
||||
} |
||||
} |
||||
|
||||
//@Preview |
||||
@Composable |
||||
fun QuantitySelectorPreview() { |
||||
JetsnackTheme { |
||||
JetsnackSurface { |
||||
QuantitySelector(1, {}, {}) |
||||
} |
||||
} |
||||
} |
||||
|
||||
//@Preview |
||||
@Composable |
||||
fun QuantitySelectorPreviewRtl() { |
||||
JetsnackTheme { |
||||
JetsnackSurface { |
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { |
||||
QuantitySelector(1, {}, {}) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,80 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope |
||||
import androidx.compose.foundation.layout.PaddingValues |
||||
import androidx.compose.material.DrawerDefaults |
||||
import androidx.compose.material.ExperimentalMaterialApi |
||||
import androidx.compose.material.FabPosition |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Scaffold |
||||
import androidx.compose.material.ScaffoldState |
||||
import androidx.compose.material.SnackbarHost |
||||
import androidx.compose.material.SnackbarHostState |
||||
import androidx.compose.material.rememberScaffoldState |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.graphics.Shape |
||||
import androidx.compose.ui.unit.Dp |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
/** |
||||
* Wrap Material [androidx.compose.material.Scaffold] and set [JetsnackTheme] colors. |
||||
*/ |
||||
@OptIn(ExperimentalMaterialApi::class) |
||||
@Composable |
||||
fun JetsnackScaffold( |
||||
modifier: Modifier = Modifier, |
||||
scaffoldState: ScaffoldState = rememberScaffoldState(), |
||||
topBar: @Composable (() -> Unit) = {}, |
||||
bottomBar: @Composable (() -> Unit) = {}, |
||||
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, |
||||
floatingActionButton: @Composable (() -> Unit) = {}, |
||||
floatingActionButtonPosition: FabPosition = FabPosition.End, |
||||
isFloatingActionButtonDocked: Boolean = false, |
||||
drawerContent: @Composable (ColumnScope.() -> Unit)? = null, |
||||
drawerShape: Shape = MaterialTheme.shapes.large, |
||||
drawerElevation: Dp = DrawerDefaults.Elevation, |
||||
drawerBackgroundColor: Color = JetsnackTheme.colors.uiBackground, |
||||
drawerContentColor: Color = JetsnackTheme.colors.textSecondary, |
||||
drawerScrimColor: Color = JetsnackTheme.colors.uiBorder, |
||||
backgroundColor: Color = JetsnackTheme.colors.uiBackground, |
||||
contentColor: Color = JetsnackTheme.colors.textSecondary, |
||||
content: @Composable (PaddingValues) -> Unit |
||||
) { |
||||
Scaffold( |
||||
modifier = modifier, |
||||
scaffoldState = scaffoldState, |
||||
topBar = topBar, |
||||
bottomBar = bottomBar, |
||||
snackbarHost = snackbarHost, |
||||
floatingActionButton = floatingActionButton, |
||||
floatingActionButtonPosition = floatingActionButtonPosition, |
||||
isFloatingActionButtonDocked = isFloatingActionButtonDocked, |
||||
drawerContent = drawerContent, |
||||
drawerShape = drawerShape, |
||||
drawerElevation = drawerElevation, |
||||
drawerBackgroundColor = drawerBackgroundColor, |
||||
drawerContentColor = drawerContentColor, |
||||
drawerScrimColor = drawerScrimColor, |
||||
backgroundColor = backgroundColor, |
||||
contentColor = contentColor, |
||||
content = content |
||||
) |
||||
} |
@ -0,0 +1,55 @@
|
||||
/* |
||||
* Copyright 2021 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Snackbar |
||||
import androidx.compose.material.SnackbarData |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.graphics.Shape |
||||
import androidx.compose.ui.unit.Dp |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
/** |
||||
* An alternative to [androidx.compose.material.Snackbar] utilizing |
||||
* [com.example.jetsnack.ui.theme.JetsnackColors] |
||||
*/ |
||||
@Composable |
||||
fun JetsnackSnackbar( |
||||
snackbarData: SnackbarData, |
||||
modifier: Modifier = Modifier, |
||||
actionOnNewLine: Boolean = false, |
||||
shape: Shape = MaterialTheme.shapes.small, |
||||
backgroundColor: Color = JetsnackTheme.colors.uiBackground, |
||||
contentColor: Color = JetsnackTheme.colors.textSecondary, |
||||
actionColor: Color = JetsnackTheme.colors.brand, |
||||
elevation: Dp = 6.dp |
||||
) { |
||||
Snackbar( |
||||
snackbarData = snackbarData, |
||||
modifier = modifier, |
||||
actionOnNewLine = actionOnNewLine, |
||||
shape = shape, |
||||
backgroundColor = backgroundColor, |
||||
contentColor = contentColor, |
||||
actionColor = actionColor, |
||||
elevation = elevation |
||||
) |
||||
} |
@ -0,0 +1,308 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
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 |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.heightIn |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.size |
||||
import androidx.compose.foundation.layout.wrapContentWidth |
||||
import androidx.compose.foundation.lazy.LazyRow |
||||
import androidx.compose.foundation.lazy.items |
||||
import androidx.compose.foundation.lazy.itemsIndexed |
||||
import androidx.compose.foundation.rememberScrollState |
||||
import androidx.compose.foundation.shape.CircleShape |
||||
import androidx.compose.material.Icon |
||||
import androidx.compose.material.IconButton |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.outlined.ArrowBack |
||||
import androidx.compose.material.icons.outlined.ArrowForward |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.platform.LocalDensity |
||||
import androidx.compose.ui.text.style.TextOverflow |
||||
import androidx.compose.ui.unit.Dp |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.model.CollectionType |
||||
import com.example.jetsnack.model.Snack |
||||
import com.example.jetsnack.model.SnackCollection |
||||
import com.example.jetsnack.model.snacks |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
import com.example.jetsnack.ui.utils.mirroringIcon |
||||
|
||||
private val HighlightCardWidth = 170.dp |
||||
private val HighlightCardPadding = 16.dp |
||||
|
||||
// The Cards show a gradient which spans 3 cards and scrolls with parallax. |
||||
private val gradientWidth |
||||
@Composable |
||||
get() = with(LocalDensity.current) { |
||||
(3 * (HighlightCardWidth + HighlightCardPadding).toPx()) |
||||
} |
||||
|
||||
@Composable |
||||
fun SnackCollection( |
||||
snackCollection: SnackCollection, |
||||
onSnackClick: (Long) -> Unit, |
||||
modifier: Modifier = Modifier, |
||||
index: Int = 0, |
||||
highlight: Boolean = true |
||||
) { |
||||
Column(modifier = modifier) { |
||||
Row( |
||||
verticalAlignment = Alignment.CenterVertically, |
||||
modifier = Modifier |
||||
.heightIn(min = 56.dp) |
||||
.padding(start = 24.dp) |
||||
) { |
||||
Text( |
||||
text = snackCollection.name, |
||||
style = MaterialTheme.typography.h6, |
||||
color = JetsnackTheme.colors.brand, |
||||
maxLines = 1, |
||||
overflow = TextOverflow.Ellipsis, |
||||
modifier = Modifier |
||||
.weight(1f) |
||||
.wrapContentWidth(Alignment.Start) |
||||
) |
||||
IconButton( |
||||
onClick = { /* todo */ }, |
||||
modifier = Modifier.align(Alignment.CenterVertically) |
||||
) { |
||||
Icon( |
||||
imageVector = mirroringIcon( |
||||
ltrIcon = Icons.Outlined.ArrowForward, |
||||
rtlIcon = Icons.Outlined.ArrowBack |
||||
), |
||||
tint = JetsnackTheme.colors.brand, |
||||
contentDescription = null |
||||
) |
||||
} |
||||
} |
||||
if (highlight && snackCollection.type == CollectionType.Highlight) { |
||||
HighlightedSnacks(index, snackCollection.snacks, onSnackClick) |
||||
} else { |
||||
Snacks(snackCollection.snacks, onSnackClick) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun HighlightedSnacks( |
||||
index: Int, |
||||
snacks: List<Snack>, |
||||
onSnackClick: (Long) -> Unit, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
val scroll = rememberScrollState(0) |
||||
val gradient = when ((index / 2) % 2) { |
||||
0 -> JetsnackTheme.colors.gradient6_1 |
||||
else -> JetsnackTheme.colors.gradient6_2 |
||||
} |
||||
// The Cards show a gradient which spans 3 cards and scrolls with parallax. |
||||
val gradientWidth = with(LocalDensity.current) { |
||||
(6 * (HighlightCardWidth + HighlightCardPadding).toPx()) |
||||
} |
||||
LazyRow( |
||||
modifier = modifier, |
||||
horizontalArrangement = Arrangement.spacedBy(16.dp), |
||||
contentPadding = PaddingValues(start = 24.dp, end = 24.dp) |
||||
) { |
||||
itemsIndexed(snacks) { index, snack -> |
||||
HighlightSnackItem( |
||||
snack, |
||||
onSnackClick, |
||||
index, |
||||
gradient, |
||||
gradientWidth, |
||||
scroll.value |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun Snacks( |
||||
snacks: List<Snack>, |
||||
onSnackClick: (Long) -> Unit, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
LazyRow( |
||||
modifier = modifier, |
||||
contentPadding = PaddingValues(start = 12.dp, end = 12.dp) |
||||
) { |
||||
items(snacks) { snack -> |
||||
SnackItem(snack, onSnackClick) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun SnackItem( |
||||
snack: Snack, |
||||
onSnackClick: (Long) -> Unit, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
JetsnackSurface( |
||||
shape = MaterialTheme.shapes.medium, |
||||
modifier = modifier.padding( |
||||
start = 4.dp, |
||||
end = 4.dp, |
||||
bottom = 8.dp |
||||
) |
||||
) { |
||||
Column( |
||||
horizontalAlignment = Alignment.CenterHorizontally, |
||||
modifier = Modifier |
||||
.clickable(onClick = { onSnackClick(snack.id) }) |
||||
.padding(8.dp) |
||||
) { |
||||
SnackImage( |
||||
imageUrl = snack.imageUrl, |
||||
elevation = 4.dp, |
||||
contentDescription = null, |
||||
modifier = Modifier.size(120.dp) |
||||
) |
||||
Text( |
||||
text = snack.name, |
||||
style = MaterialTheme.typography.subtitle1, |
||||
color = JetsnackTheme.colors.textSecondary, |
||||
modifier = Modifier.padding(top = 8.dp) |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun HighlightSnackItem( |
||||
snack: Snack, |
||||
onSnackClick: (Long) -> Unit, |
||||
index: Int, |
||||
gradient: List<Color>, |
||||
gradientWidth: Float, |
||||
scroll: Int, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
val left = index * with(LocalDensity.current) { |
||||
(HighlightCardWidth + HighlightCardPadding).toPx() |
||||
} |
||||
JetsnackCard( |
||||
modifier = modifier |
||||
.size( |
||||
width = 170.dp, |
||||
height = 250.dp |
||||
) |
||||
.padding(bottom = 16.dp) |
||||
) { |
||||
Column( |
||||
modifier = Modifier |
||||
.clickable(onClick = { onSnackClick(snack.id) }) |
||||
.fillMaxSize() |
||||
) { |
||||
Box( |
||||
modifier = Modifier |
||||
.height(160.dp) |
||||
.fillMaxWidth() |
||||
) { |
||||
val gradientOffset = left - (scroll / 3f) |
||||
Box( |
||||
modifier = Modifier |
||||
.height(100.dp) |
||||
.fillMaxWidth() |
||||
.offsetGradientBackground(gradient, gradientWidth, gradientOffset) |
||||
) |
||||
SnackImage( |
||||
imageUrl = snack.imageUrl, |
||||
contentDescription = null, |
||||
modifier = Modifier |
||||
.size(120.dp) |
||||
.align(Alignment.BottomCenter) |
||||
) |
||||
} |
||||
Spacer(modifier = Modifier.height(8.dp)) |
||||
Text( |
||||
text = snack.name, |
||||
maxLines = 1, |
||||
overflow = TextOverflow.Ellipsis, |
||||
style = MaterialTheme.typography.h6, |
||||
color = JetsnackTheme.colors.textSecondary, |
||||
modifier = Modifier.padding(horizontal = 16.dp) |
||||
) |
||||
Spacer(modifier = Modifier.height(4.dp)) |
||||
Text( |
||||
text = snack.tagline, |
||||
style = MaterialTheme.typography.body1, |
||||
color = JetsnackTheme.colors.textHelp, |
||||
modifier = Modifier.padding(horizontal = 16.dp) |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun SnackImage( |
||||
imageUrl: String, |
||||
contentDescription: String?, |
||||
modifier: Modifier = Modifier, |
||||
elevation: Dp = 0.dp |
||||
) { |
||||
JetsnackSurface( |
||||
color = Color.LightGray, |
||||
elevation = elevation, |
||||
shape = CircleShape, |
||||
modifier = modifier |
||||
) { |
||||
SnackAsyncImage(imageUrl, contentDescription, Modifier.fillMaxSize()) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
expect fun SnackAsyncImage( |
||||
imageUrl: String, |
||||
contentDescription: String?, |
||||
modifier: Modifier |
||||
) |
||||
|
||||
//@Preview |
||||
@Composable |
||||
fun SnackCardPreview() { |
||||
JetsnackTheme { |
||||
val snack = snacks.first() |
||||
HighlightSnackItem( |
||||
snack = snack, |
||||
onSnackClick = { }, |
||||
index = 0, |
||||
gradient = JetsnackTheme.colors.gradient6_1, |
||||
gradientWidth = gradientWidth, |
||||
scroll = 0 |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,97 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
import androidx.compose.foundation.BorderStroke |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.border |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.material.LocalContentColor |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.CompositionLocalProvider |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.draw.clip |
||||
import androidx.compose.ui.draw.shadow |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.graphics.RectangleShape |
||||
import androidx.compose.ui.graphics.Shape |
||||
import androidx.compose.ui.graphics.compositeOver |
||||
import androidx.compose.ui.unit.Dp |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.compose.ui.zIndex |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
import kotlin.math.ln |
||||
|
||||
/** |
||||
* An alternative to [androidx.compose.material.Surface] utilizing |
||||
* [com.example.jetsnack.ui.theme.JetsnackColors] |
||||
*/ |
||||
@Composable |
||||
fun JetsnackSurface( |
||||
modifier: Modifier = Modifier, |
||||
shape: Shape = RectangleShape, |
||||
color: Color = JetsnackTheme.colors.uiBackground, |
||||
contentColor: Color = JetsnackTheme.colors.textSecondary, |
||||
border: BorderStroke? = null, |
||||
elevation: Dp = 0.dp, |
||||
content: @Composable () -> Unit |
||||
) { |
||||
Box( |
||||
modifier = modifier.shadow(elevation = elevation, shape = shape, clip = false) |
||||
.zIndex(elevation.value) |
||||
.then(if (border != null) Modifier.border(border, shape) else Modifier) |
||||
.background( |
||||
color = getBackgroundColorForElevation(color, elevation), |
||||
shape = shape |
||||
) |
||||
.clip(shape) |
||||
) { |
||||
CompositionLocalProvider(LocalContentColor provides contentColor, content = content) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun getBackgroundColorForElevation(color: Color, elevation: Dp): Color { |
||||
return if (elevation > 0.dp // && https://issuetracker.google.com/issues/161429530 |
||||
// JetsnackTheme.colors.isDark //&& |
||||
// color == JetsnackTheme.colors.uiBackground |
||||
) { |
||||
color.withElevation(elevation) |
||||
} else { |
||||
color |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Applies a [Color.White] overlay to this color based on the [elevation]. This increases visibility |
||||
* of elevation for surfaces in a dark theme. |
||||
* |
||||
* TODO: Remove when public https://issuetracker.google.com/155181601 |
||||
*/ |
||||
private fun Color.withElevation(elevation: Dp): Color { |
||||
val foreground = calculateForeground(elevation) |
||||
return foreground.compositeOver(this) |
||||
} |
||||
|
||||
/** |
||||
* @return the alpha-modified [Color.White] to overlay on top of the surface color to produce |
||||
* the resultant color. |
||||
*/ |
||||
private fun calculateForeground(elevation: Dp): Color { |
||||
val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f |
||||
return Color.White.copy(alpha = alpha) |
||||
} |
@ -0,0 +1,82 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.home |
||||
|
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.material.Icon |
||||
import androidx.compose.material.IconButton |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.material.TopAppBar |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.text.style.TextAlign |
||||
import androidx.compose.ui.text.style.TextOverflow |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.ExpandMore |
||||
import com.example.jetsnack.MppR |
||||
import com.example.jetsnack.label_select_delivery |
||||
import com.example.jetsnack.stringResource |
||||
import com.example.jetsnack.ui.components.JetsnackDivider |
||||
import com.example.jetsnack.ui.snackdetail.jetSnackStatusBarsPadding |
||||
import com.example.jetsnack.ui.theme.AlphaNearOpaque |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
@Composable |
||||
fun DestinationBar(modifier: Modifier = Modifier) { |
||||
Column(modifier = modifier.jetSnackStatusBarsPadding()) { |
||||
TopAppBar( |
||||
backgroundColor = JetsnackTheme.colors.uiBackground.copy(alpha = AlphaNearOpaque), |
||||
contentColor = JetsnackTheme.colors.textSecondary, |
||||
elevation = 0.dp |
||||
) { |
||||
Text( |
||||
text = "Huidekoperstraat 26-28, 1017 ZM Amsterdam | https://kotl.in/wasm-gio23", |
||||
style = MaterialTheme.typography.subtitle1, |
||||
color = JetsnackTheme.colors.textSecondary, |
||||
textAlign = TextAlign.Center, |
||||
maxLines = 1, |
||||
overflow = TextOverflow.Ellipsis, |
||||
modifier = Modifier |
||||
.weight(1f) |
||||
.align(Alignment.CenterVertically) |
||||
) |
||||
IconButton( |
||||
onClick = { /* todo */ }, |
||||
modifier = Modifier.align(Alignment.CenterVertically) |
||||
) { |
||||
Icon( |
||||
imageVector = Icons.Outlined.ExpandMore, |
||||
tint = JetsnackTheme.colors.brand, |
||||
contentDescription = stringResource(MppR.string.label_select_delivery) |
||||
) |
||||
} |
||||
} |
||||
JetsnackDivider() |
||||
} |
||||
} |
||||
|
||||
//@Preview |
||||
@Composable |
||||
fun PreviewDestinationBar() { |
||||
JetsnackTheme { |
||||
DestinationBar() |
||||
} |
||||
} |
@ -0,0 +1,137 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.home |
||||
|
||||
import androidx.compose.animation.AnimatedVisibility |
||||
import androidx.compose.animation.ExperimentalAnimationApi |
||||
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.desktop.ui.tooling.preview.Preview |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.WindowInsets |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.windowInsetsTopHeight |
||||
import androidx.compose.foundation.lazy.LazyColumn |
||||
import androidx.compose.foundation.lazy.itemsIndexed |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.mutableStateOf |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.saveable.rememberSaveable |
||||
import androidx.compose.runtime.setValue |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.model.Filter |
||||
import com.example.jetsnack.model.SnackCollection |
||||
import com.example.jetsnack.model.SnackRepo |
||||
import com.example.jetsnack.ui.components.FilterBar |
||||
import com.example.jetsnack.ui.components.JetsnackDivider |
||||
import com.example.jetsnack.ui.components.JetsnackSurface |
||||
import com.example.jetsnack.ui.components.SnackCollection |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
@Composable |
||||
fun Feed( |
||||
onSnackClick: (Long) -> Unit, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
val snackCollections = remember { SnackRepo.getSnacks() } |
||||
val filters = remember { SnackRepo.getFilters() } |
||||
Feed( |
||||
snackCollections, |
||||
filters, |
||||
onSnackClick, |
||||
modifier |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
private fun Feed( |
||||
snackCollections: List<SnackCollection>, |
||||
filters: List<Filter>, |
||||
onSnackClick: (Long) -> Unit, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
|
||||
JetsnackSurface(modifier = modifier.fillMaxSize()) { |
||||
Box { |
||||
SnackCollectionList(snackCollections, filters, onSnackClick) |
||||
DestinationBar() |
||||
} |
||||
} |
||||
} |
||||
|
||||
@OptIn(ExperimentalAnimationApi::class) |
||||
@Composable |
||||
private fun SnackCollectionList( |
||||
snackCollections: List<SnackCollection>, |
||||
filters: List<Filter>, |
||||
onSnackClick: (Long) -> Unit, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
var filtersVisible by rememberSaveable { mutableStateOf(false) } |
||||
Box(modifier) { |
||||
LazyColumn { |
||||
|
||||
item { |
||||
Spacer( |
||||
Modifier.windowInsetsTopHeight(snackCollectionListItemWindowInsets()) |
||||
) |
||||
FilterBar(filters, onShowFilters = { filtersVisible = true }) |
||||
} |
||||
itemsIndexed(snackCollections) { index, snackCollection -> |
||||
if (index > 0) { |
||||
JetsnackDivider(thickness = 2.dp) |
||||
} |
||||
|
||||
SnackCollection( |
||||
snackCollection = snackCollection, |
||||
onSnackClick = onSnackClick, |
||||
index = index |
||||
) |
||||
} |
||||
} |
||||
} |
||||
AnimatedVisibility( |
||||
visible = filtersVisible, |
||||
enter = slideInVertically() + expandVertically( |
||||
expandFrom = Alignment.Top |
||||
) + fadeIn(initialAlpha = 0.3f), |
||||
exit = slideOutVertically() + shrinkVertically() + fadeOut() |
||||
) { |
||||
FilterScreen( |
||||
onDismiss = { filtersVisible = false } |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
expect fun snackCollectionListItemWindowInsets(): WindowInsets |
||||
|
||||
//@Preview |
||||
@Composable |
||||
fun HomePreview() { |
||||
JetsnackTheme { |
||||
Feed(onSnackClick = { }) |
||||
} |
||||
} |
@ -0,0 +1,275 @@
|
||||
/* |
||||
* Copyright 2021 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.home |
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi |
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
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.rememberScrollState |
||||
import androidx.compose.foundation.selection.selectable |
||||
import androidx.compose.foundation.verticalScroll |
||||
import androidx.compose.material.ContentAlpha |
||||
import androidx.compose.material.Icon |
||||
import androidx.compose.material.IconButton |
||||
import androidx.compose.material.LocalContentAlpha |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Slider |
||||
import androidx.compose.material.SliderDefaults |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.material.TopAppBar |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.filled.Close |
||||
import androidx.compose.material.icons.filled.Done |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.CompositionLocalProvider |
||||
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.graphics.vector.ImageVector |
||||
import androidx.compose.ui.text.style.TextAlign |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.* |
||||
import com.example.jetsnack.flowlayout.FlowMainAxisAlignment |
||||
import com.example.jetsnack.flowlayout.FlowRow |
||||
import com.example.jetsnack.model.Filter |
||||
import com.example.jetsnack.model.SnackRepo |
||||
import com.example.jetsnack.ui.components.FilterChip |
||||
import com.example.jetsnack.ui.components.JetsnackScaffold |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
@OptIn(ExperimentalAnimationApi::class) |
||||
@Composable |
||||
fun FilterScreen( |
||||
onDismiss: () -> Unit |
||||
) { |
||||
var sortState by remember { mutableStateOf(SnackRepo.getSortDefault()) } |
||||
var maxCalories by remember { mutableStateOf(0f) } |
||||
val defaultFilter = SnackRepo.getSortDefault() |
||||
|
||||
SnackDialog(onCloseRequest = onDismiss) { |
||||
val priceFilters = remember { SnackRepo.getPriceFilters() } |
||||
val categoryFilters = remember { SnackRepo.getCategoryFilters() } |
||||
val lifeStyleFilters = remember { SnackRepo.getLifeStyleFilters() } |
||||
JetsnackScaffold( |
||||
topBar = { |
||||
TopAppBar( |
||||
navigationIcon = { |
||||
IconButton(onClick = onDismiss) { |
||||
Icon( |
||||
imageVector = Icons.Filled.Close, |
||||
contentDescription = stringResource(id = MppR.string.close) |
||||
) |
||||
} |
||||
}, |
||||
title = { |
||||
Text( |
||||
text = stringResource(id = MppR.string.label_filters), |
||||
modifier = Modifier.fillMaxWidth(), |
||||
textAlign = TextAlign.Center, |
||||
style = MaterialTheme.typography.h6 |
||||
) |
||||
}, |
||||
actions = { |
||||
var resetEnabled = sortState != defaultFilter |
||||
IconButton( |
||||
onClick = { /* TODO: Open search */ }, |
||||
enabled = resetEnabled |
||||
) { |
||||
val alpha = if (resetEnabled) { |
||||
ContentAlpha.high |
||||
} else { |
||||
ContentAlpha.disabled |
||||
} |
||||
CompositionLocalProvider(LocalContentAlpha provides alpha) { |
||||
Text( |
||||
text = stringResource(id = MppR.string.reset), |
||||
style = MaterialTheme.typography.body2 |
||||
) |
||||
} |
||||
} |
||||
}, |
||||
backgroundColor = JetsnackTheme.colors.uiBackground |
||||
) |
||||
} |
||||
) { |
||||
Column( |
||||
Modifier |
||||
.fillMaxSize() |
||||
.verticalScroll(rememberScrollState()) |
||||
.padding(horizontal = 24.dp, vertical = 16.dp), |
||||
) { |
||||
SortFiltersSection( |
||||
sortState = sortState, |
||||
onFilterChange = { filter -> |
||||
sortState = filter.name |
||||
} |
||||
) |
||||
FilterChipSection( |
||||
title = stringResource(id = MppR.string.price), |
||||
filters = priceFilters |
||||
) |
||||
FilterChipSection( |
||||
title = stringResource(id = MppR.string.category), |
||||
filters = categoryFilters |
||||
) |
||||
|
||||
MaxCalories( |
||||
sliderPosition = maxCalories, |
||||
onValueChanged = { newValue -> |
||||
maxCalories = newValue |
||||
} |
||||
) |
||||
FilterChipSection( |
||||
title = stringResource(id = MppR.string.lifestyle), |
||||
filters = lifeStyleFilters |
||||
) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
expect fun SnackDialog(onCloseRequest: () -> Unit, content: @Composable () -> Unit) |
||||
|
||||
|
||||
@Composable |
||||
fun FilterChipSection(title: String, filters: List<Filter>) { |
||||
FilterTitle(text = title) |
||||
FlowRow( |
||||
mainAxisAlignment = FlowMainAxisAlignment.Center, |
||||
modifier = Modifier |
||||
.fillMaxWidth() |
||||
.padding(top = 12.dp, bottom = 16.dp) |
||||
.padding(horizontal = 4.dp) |
||||
) { |
||||
filters.forEach { filter -> |
||||
FilterChip( |
||||
filter = filter, |
||||
modifier = Modifier.padding(end = 4.dp, bottom = 8.dp) |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun SortFiltersSection(sortState: String, onFilterChange: (Filter) -> Unit) { |
||||
FilterTitle(text = stringResource(id = MppR.string.sort)) |
||||
Column(Modifier.padding(bottom = 24.dp)) { |
||||
SortFilters( |
||||
sortState = sortState, |
||||
onChanged = onFilterChange |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun SortFilters( |
||||
sortFilters: List<Filter> = SnackRepo.getSortFilters(), |
||||
sortState: String, |
||||
onChanged: (Filter) -> Unit |
||||
) { |
||||
|
||||
sortFilters.forEach { filter -> |
||||
SortOption( |
||||
text = filter.name, |
||||
icon = filter.icon, |
||||
selected = sortState == filter.name, |
||||
onClickOption = { |
||||
onChanged(filter) |
||||
} |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun MaxCalories(sliderPosition: Float, onValueChanged: (Float) -> Unit) { |
||||
FlowRow { |
||||
FilterTitle(text = stringResource(id = MppR.string.max_calories)) |
||||
Text( |
||||
text = stringResource(id = MppR.string.per_serving), |
||||
style = MaterialTheme.typography.body2, |
||||
color = JetsnackTheme.colors.brand, |
||||
modifier = Modifier.padding(top = 5.dp, start = 10.dp) |
||||
) |
||||
} |
||||
Slider( |
||||
value = sliderPosition, |
||||
onValueChange = { newValue -> |
||||
onValueChanged(newValue) |
||||
}, |
||||
valueRange = 0f..300f, |
||||
steps = 5, |
||||
modifier = Modifier |
||||
.fillMaxWidth(), |
||||
colors = SliderDefaults.colors( |
||||
thumbColor = JetsnackTheme.colors.brand, |
||||
activeTrackColor = JetsnackTheme.colors.brand |
||||
) |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
fun FilterTitle(text: String) { |
||||
Text( |
||||
text = text, |
||||
style = MaterialTheme.typography.h6, |
||||
color = JetsnackTheme.colors.brand, |
||||
modifier = Modifier.padding(bottom = 8.dp) |
||||
) |
||||
} |
||||
@Composable |
||||
fun SortOption( |
||||
text: String, |
||||
icon: ImageVector?, |
||||
onClickOption: () -> Unit, |
||||
selected: Boolean |
||||
) { |
||||
Row( |
||||
modifier = Modifier |
||||
.padding(top = 14.dp) |
||||
.selectable(selected) { onClickOption() } |
||||
) { |
||||
if (icon != null) { |
||||
Icon(imageVector = icon, contentDescription = null) |
||||
} |
||||
Text( |
||||
text = text, |
||||
style = MaterialTheme.typography.subtitle1, |
||||
modifier = Modifier |
||||
.padding(start = 10.dp) |
||||
.weight(1f) |
||||
) |
||||
if (selected) { |
||||
Icon( |
||||
imageVector = Icons.Filled.Done, |
||||
contentDescription = null, |
||||
tint = JetsnackTheme.colors.brand |
||||
) |
||||
} |
||||
} |
||||
} |
||||
//@Preview |
||||
@Composable |
||||
fun FilterScreenPreview() { |
||||
FilterScreen(onDismiss = {}) |
||||
} |
@ -0,0 +1,363 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.home |
||||
|
||||
import androidx.compose.animation.animateColorAsState |
||||
import androidx.compose.animation.core.Animatable |
||||
import androidx.compose.animation.core.AnimationSpec |
||||
import androidx.compose.animation.core.SpringSpec |
||||
import androidx.compose.animation.core.animateFloatAsState |
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
import androidx.compose.foundation.border |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.BoxScope |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.wrapContentSize |
||||
import androidx.compose.foundation.selection.selectable |
||||
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
import androidx.compose.material.Icon |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.outlined.AccountCircle |
||||
import androidx.compose.material.icons.outlined.Home |
||||
import androidx.compose.material.icons.outlined.Search |
||||
import androidx.compose.material.icons.outlined.ShoppingCart |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.LaunchedEffect |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.draw.clip |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.graphics.Shape |
||||
import androidx.compose.ui.graphics.TransformOrigin |
||||
import androidx.compose.ui.graphics.graphicsLayer |
||||
import androidx.compose.ui.graphics.vector.ImageVector |
||||
import androidx.compose.ui.layout.Layout |
||||
import androidx.compose.ui.layout.MeasureResult |
||||
import androidx.compose.ui.layout.MeasureScope |
||||
import androidx.compose.ui.layout.Placeable |
||||
import androidx.compose.ui.layout.layoutId |
||||
import androidx.compose.ui.unit.Dp |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.* |
||||
import com.example.jetsnack.ui.components.JetsnackSurface |
||||
import com.example.jetsnack.ui.snackdetail.jetSnackNavigationBarsPadding |
||||
import com.example.jetsnack.ui.snackdetail.lerp |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
//fun NavGraphBuilder.addHomeGraph( |
||||
// onSnackSelected: (Long, NavBackStackEntry) -> Unit, |
||||
// modifier: Modifier = Modifier |
||||
//) { |
||||
// composable(HomeSections.FEED.route) { from -> |
||||
// Feed(onSnackClick = { id -> onSnackSelected(id, from) }, modifier) |
||||
// } |
||||
// composable(HomeSections.SEARCH.route) { from -> |
||||
// Search(onSnackClick = { id -> onSnackSelected(id, from) }, modifier) |
||||
// } |
||||
// composable(HomeSections.CART.route) { from -> |
||||
// Cart(onSnackClick = { id -> onSnackSelected(id, from) }, modifier) |
||||
// } |
||||
// composable(HomeSections.PROFILE.route) { |
||||
// Profile(modifier) |
||||
// } |
||||
//} |
||||
|
||||
enum class HomeSections( |
||||
val title: Int, // @StringRes |
||||
val icon: ImageVector, |
||||
val route: String |
||||
) { |
||||
FEED(MppR.string.home_feed, Icons.Outlined.Home, "home/feed"), |
||||
SEARCH(MppR.string.home_search, Icons.Outlined.Search, "home/search"), |
||||
CART(MppR.string.home_cart, Icons.Outlined.ShoppingCart, "home/cart"), |
||||
PROFILE(MppR.string.home_profile, Icons.Outlined.AccountCircle, "home/profile") |
||||
} |
||||
|
||||
@Composable |
||||
fun JetsnackBottomBar( |
||||
tabs: Array<HomeSections>, |
||||
currentRoute: String, |
||||
navigateToRoute: (String) -> Unit, |
||||
color: Color = JetsnackTheme.colors.iconPrimary, |
||||
contentColor: Color = JetsnackTheme.colors.iconInteractive |
||||
) { |
||||
val routes = remember { tabs.map { it.route } } |
||||
val currentSection = tabs.first { it.route == currentRoute } |
||||
|
||||
JetsnackSurface( |
||||
color = color, |
||||
contentColor = contentColor |
||||
) { |
||||
val springSpec = SpringSpec<Float>( |
||||
// Determined experimentally |
||||
stiffness = 800f, |
||||
dampingRatio = 0.8f |
||||
) |
||||
JetsnackBottomNavLayout( |
||||
selectedIndex = currentSection.ordinal, |
||||
itemCount = routes.size, |
||||
indicator = { JetsnackBottomNavIndicator() }, |
||||
animSpec = springSpec, |
||||
modifier = Modifier.jetSnackNavigationBarsPadding() |
||||
) { |
||||
// TODO: implement getting currentLocale in common source set |
||||
// val configuration = LocalConfiguration.current |
||||
// val currentLocale: Locale = |
||||
// ConfigurationCompat.getLocales(configuration).get(0) ?: Locale.getDefault() |
||||
|
||||
tabs.forEach { section -> |
||||
val selected = section == currentSection |
||||
val tint by animateColorAsState( |
||||
if (selected) { |
||||
JetsnackTheme.colors.iconInteractive |
||||
} else { |
||||
JetsnackTheme.colors.iconInteractiveInactive |
||||
} |
||||
) |
||||
|
||||
// TODO: implement uppercase using currentLocale |
||||
// val text = stringResource(section.title).uppercase(currentLocale) |
||||
val text = stringResource(section.title).uppercase() |
||||
|
||||
JetsnackBottomNavigationItem( |
||||
icon = { |
||||
Icon( |
||||
imageVector = section.icon, |
||||
tint = tint, |
||||
contentDescription = text |
||||
) |
||||
}, |
||||
text = { |
||||
Text( |
||||
text = text, |
||||
color = tint, |
||||
style = MaterialTheme.typography.button, |
||||
maxLines = 1 |
||||
) |
||||
}, |
||||
selected = selected, |
||||
onSelected = { navigateToRoute(section.route) }, |
||||
animSpec = springSpec, |
||||
modifier = BottomNavigationItemPadding |
||||
.clip(BottomNavIndicatorShape) |
||||
) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun JetsnackBottomNavLayout( |
||||
selectedIndex: Int, |
||||
itemCount: Int, |
||||
animSpec: AnimationSpec<Float>, |
||||
indicator: @Composable BoxScope.() -> Unit, |
||||
modifier: Modifier = Modifier, |
||||
content: @Composable () -> Unit |
||||
) { |
||||
// Track how "selected" each item is [0, 1] |
||||
val selectionFractions = remember(itemCount) { |
||||
List(itemCount) { i -> |
||||
Animatable(if (i == selectedIndex) 1f else 0f) |
||||
} |
||||
} |
||||
selectionFractions.forEachIndexed { index, selectionFraction -> |
||||
val target = if (index == selectedIndex) 1f else 0f |
||||
LaunchedEffect(target, animSpec) { |
||||
selectionFraction.animateTo(target, animSpec) |
||||
} |
||||
} |
||||
|
||||
// Animate the position of the indicator |
||||
val indicatorIndex = remember { Animatable(0f) } |
||||
val targetIndicatorIndex = selectedIndex.toFloat() |
||||
LaunchedEffect(targetIndicatorIndex) { |
||||
indicatorIndex.animateTo(targetIndicatorIndex, animSpec) |
||||
} |
||||
|
||||
Layout( |
||||
modifier = modifier.height(BottomNavHeight), |
||||
content = { |
||||
content() |
||||
Box(Modifier.layoutId("indicator"), content = indicator) |
||||
} |
||||
) { measurables, constraints -> |
||||
check(itemCount == (measurables.size - 1)) // account for indicator |
||||
|
||||
// Divide the width into n+1 slots and give the selected item 2 slots |
||||
val unselectedWidth = constraints.maxWidth / (itemCount + 1) |
||||
val selectedWidth = 2 * unselectedWidth |
||||
val indicatorMeasurable = measurables.first { it.layoutId == "indicator" } |
||||
|
||||
val itemPlaceables = measurables |
||||
.filterNot { it == indicatorMeasurable } |
||||
.mapIndexed { index, measurable -> |
||||
// Animate item's width based upon the selection amount |
||||
val width = lerp(unselectedWidth, selectedWidth, selectionFractions[index].value) |
||||
measurable.measure( |
||||
constraints.copy( |
||||
minWidth = width, |
||||
maxWidth = width |
||||
) |
||||
) |
||||
} |
||||
val indicatorPlaceable = indicatorMeasurable.measure( |
||||
constraints.copy( |
||||
minWidth = selectedWidth, |
||||
maxWidth = selectedWidth |
||||
) |
||||
) |
||||
|
||||
layout( |
||||
width = constraints.maxWidth, |
||||
height = itemPlaceables.maxByOrNull { it.height }?.height ?: 0 |
||||
) { |
||||
val indicatorLeft = indicatorIndex.value * unselectedWidth |
||||
indicatorPlaceable.placeRelative(x = indicatorLeft.toInt(), y = 0) |
||||
var x = 0 |
||||
itemPlaceables.forEach { placeable -> |
||||
placeable.placeRelative(x = x, y = 0) |
||||
x += placeable.width |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun JetsnackBottomNavigationItem( |
||||
icon: @Composable BoxScope.() -> Unit, |
||||
text: @Composable BoxScope.() -> Unit, |
||||
selected: Boolean, |
||||
onSelected: () -> Unit, |
||||
animSpec: AnimationSpec<Float>, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
// Animate the icon/text positions within the item based on selection |
||||
val animationProgress by animateFloatAsState(if (selected) 1f else 0f, animSpec) |
||||
JetsnackBottomNavItemLayout( |
||||
icon = icon, |
||||
text = text, |
||||
animationProgress = animationProgress, |
||||
modifier = modifier |
||||
.selectable(selected = selected, onClick = onSelected) |
||||
.wrapContentSize() |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
private fun JetsnackBottomNavItemLayout( |
||||
icon: @Composable BoxScope.() -> Unit, |
||||
text: @Composable BoxScope.() -> Unit, |
||||
animationProgress: Float, // @FloatRange(from = 0.0, to = 1.0) |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
Layout( |
||||
modifier = modifier, |
||||
content = { |
||||
Box( |
||||
modifier = Modifier |
||||
.layoutId("icon") |
||||
.padding(horizontal = TextIconSpacing), |
||||
content = icon |
||||
) |
||||
val scale = lerp(0.6f, 1f, animationProgress) |
||||
Box( |
||||
modifier = Modifier |
||||
.layoutId("text") |
||||
.padding(horizontal = TextIconSpacing) |
||||
.graphicsLayer { |
||||
alpha = animationProgress |
||||
scaleX = scale |
||||
scaleY = scale |
||||
transformOrigin = BottomNavLabelTransformOrigin |
||||
}, |
||||
content = text |
||||
) |
||||
} |
||||
) { measurables, constraints -> |
||||
val iconPlaceable = measurables.first { it.layoutId == "icon" }.measure(constraints) |
||||
val textPlaceable = measurables.first { it.layoutId == "text" }.measure(constraints) |
||||
|
||||
placeTextAndIcon( |
||||
textPlaceable, |
||||
iconPlaceable, |
||||
constraints.maxWidth, |
||||
constraints.maxHeight, |
||||
animationProgress |
||||
) |
||||
} |
||||
} |
||||
|
||||
private fun MeasureScope.placeTextAndIcon( |
||||
textPlaceable: Placeable, |
||||
iconPlaceable: Placeable, |
||||
width: Int, |
||||
height: Int, |
||||
animationProgress: Float // @FloatRange(from = 0.0, to = 1.0) |
||||
): MeasureResult { |
||||
val iconY = (height - iconPlaceable.height) / 2 |
||||
val textY = (height - textPlaceable.height) / 2 |
||||
|
||||
val textWidth = textPlaceable.width * animationProgress |
||||
val iconX = (width - textWidth - iconPlaceable.width) / 2 |
||||
val textX = iconX + iconPlaceable.width |
||||
|
||||
return layout(width, height) { |
||||
iconPlaceable.placeRelative(iconX.toInt(), iconY) |
||||
if (animationProgress != 0f) { |
||||
textPlaceable.placeRelative(textX.toInt(), textY) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun JetsnackBottomNavIndicator( |
||||
strokeWidth: Dp = 2.dp, |
||||
color: Color = JetsnackTheme.colors.iconInteractive, |
||||
shape: Shape = BottomNavIndicatorShape |
||||
) { |
||||
Spacer( |
||||
modifier = Modifier |
||||
.fillMaxSize() |
||||
.then(BottomNavigationItemPadding) |
||||
.border(strokeWidth, color, shape) |
||||
) |
||||
} |
||||
|
||||
private val TextIconSpacing = 2.dp |
||||
private val BottomNavHeight = 56.dp |
||||
private val BottomNavLabelTransformOrigin = TransformOrigin(0f, 0.5f) |
||||
private val BottomNavIndicatorShape = RoundedCornerShape(percent = 50) |
||||
private val BottomNavigationItemPadding = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) |
||||
|
||||
//@Preview |
||||
@Composable |
||||
private fun JetsnackBottomNavPreview() { |
||||
JetsnackTheme { |
||||
JetsnackBottomBar( |
||||
tabs = HomeSections.values(), |
||||
currentRoute = "home/feed", |
||||
navigateToRoute = { } |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,74 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.home |
||||
|
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
import androidx.compose.foundation.Image |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.wrapContentSize |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.text.style.TextAlign |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.* |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
@Composable |
||||
fun Profile(modifier: Modifier = Modifier) { |
||||
Column( |
||||
horizontalAlignment = Alignment.CenterHorizontally, |
||||
modifier = modifier |
||||
.fillMaxSize() |
||||
.wrapContentSize() |
||||
.padding(24.dp) |
||||
) { |
||||
Image( |
||||
painterResource(MppR.drawable.empty_state_search), |
||||
contentDescription = null |
||||
) |
||||
Spacer(Modifier.height(24.dp)) |
||||
Text( |
||||
text = stringResource(MppR.string.work_in_progress), |
||||
style = MaterialTheme.typography.subtitle1, |
||||
textAlign = TextAlign.Center, |
||||
modifier = Modifier.fillMaxWidth() |
||||
) |
||||
Spacer(Modifier.height(16.dp)) |
||||
Text( |
||||
text = stringResource(MppR.string.grab_beverage), |
||||
style = MaterialTheme.typography.body2, |
||||
textAlign = TextAlign.Center, |
||||
modifier = Modifier.fillMaxWidth() |
||||
) |
||||
} |
||||
} |
||||
|
||||
//@Preview |
||||
@Composable |
||||
fun ProfilePreview() { |
||||
JetsnackTheme { |
||||
Profile() |
||||
} |
||||
} |
@ -0,0 +1,343 @@
|
||||
package com.example.jetsnack.ui.home.cart |
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi |
||||
import androidx.compose.animation.core.animateDpAsState |
||||
import androidx.compose.animation.core.animateFloatAsState |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.layout.* |
||||
import androidx.compose.foundation.lazy.LazyColumn |
||||
import androidx.compose.foundation.lazy.items |
||||
import androidx.compose.foundation.shape.CircleShape |
||||
import androidx.compose.material.Icon |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Surface |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.collectAsState |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.graphics.RectangleShape |
||||
import androidx.compose.ui.graphics.graphicsLayer |
||||
import androidx.compose.ui.layout.LastBaseline |
||||
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 com.example.jetsnack.* |
||||
import com.example.jetsnack.model.OrderLine |
||||
import com.example.jetsnack.model.SnackCollection |
||||
import com.example.jetsnack.model.SnackRepo |
||||
import com.example.jetsnack.ui.components.JetsnackButton |
||||
import com.example.jetsnack.ui.components.JetsnackDivider |
||||
import com.example.jetsnack.ui.components.JetsnackSurface |
||||
import com.example.jetsnack.ui.components.SnackCollection |
||||
import com.example.jetsnack.ui.home.DestinationBar |
||||
import com.example.jetsnack.ui.theme.AlphaNearOpaque |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
import com.example.jetsnack.ui.utils.formatPrice |
||||
|
||||
|
||||
@Composable |
||||
fun Cart( |
||||
onSnackClick: (Long) -> Unit, |
||||
modifier: Modifier = Modifier, |
||||
viewModel: CartViewModel = provideCartViewModel() |
||||
) { |
||||
val orderLines by viewModel.collectOrderLinesAsState(viewModel.orderLines) |
||||
val inspiredByCart = remember { SnackRepo.getInspiredByCart() } |
||||
Cart( |
||||
orderLines = orderLines, |
||||
removeSnack = viewModel::removeSnack, |
||||
increaseItemCount = viewModel::increaseSnackCount, |
||||
decreaseItemCount = viewModel::decreaseSnackCount, |
||||
inspiredByCart = inspiredByCart, |
||||
onSnackClick = onSnackClick, |
||||
modifier = modifier |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
expect fun provideCartViewModel(): CartViewModel |
||||
|
||||
/** |
||||
* Android uses ConstraintLayout which is android-only at the moment. |
||||
* So we provide an alternative implementation of `ActualCartItem` for other platforms. |
||||
*/ |
||||
@Composable |
||||
expect fun ActualCartItem( |
||||
orderLine: OrderLine, |
||||
removeSnack: (Long) -> Unit, |
||||
increaseItemCount: (Long) -> Unit, |
||||
decreaseItemCount: (Long) -> Unit, |
||||
onSnackClick: (Long) -> Unit, |
||||
modifier: Modifier = Modifier |
||||
) |
||||
|
||||
@Composable |
||||
fun Cart( |
||||
orderLines: List<OrderLine>, |
||||
removeSnack: (Long) -> Unit, |
||||
increaseItemCount: (Long) -> Unit, |
||||
decreaseItemCount: (Long) -> Unit, |
||||
inspiredByCart: SnackCollection, |
||||
onSnackClick: (Long) -> Unit, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
JetsnackSurface(modifier = modifier.fillMaxSize()) { |
||||
Box { |
||||
CartContent( |
||||
orderLines = orderLines, |
||||
removeSnack = removeSnack, |
||||
increaseItemCount = increaseItemCount, |
||||
decreaseItemCount = decreaseItemCount, |
||||
inspiredByCart = inspiredByCart, |
||||
onSnackClick = onSnackClick, |
||||
modifier = Modifier.align(Alignment.TopCenter) |
||||
) |
||||
DestinationBar(modifier = Modifier.align(Alignment.TopCenter)) |
||||
CheckoutBar(modifier = Modifier.align(Alignment.BottomCenter)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
expect fun rememberQuantityString(res: Int, qty: Int, vararg args: Any): String |
||||
|
||||
@Composable |
||||
expect fun getCartContentInsets(): WindowInsets |
||||
|
||||
@OptIn(ExperimentalAnimationApi::class) |
||||
@Composable |
||||
private fun CartContent( |
||||
orderLines: List<OrderLine>, |
||||
removeSnack: (Long) -> Unit, |
||||
increaseItemCount: (Long) -> Unit, |
||||
decreaseItemCount: (Long) -> Unit, |
||||
inspiredByCart: SnackCollection, |
||||
onSnackClick: (Long) -> Unit, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
val snackCountFormattedString = rememberQuantityString( |
||||
MppR.plurals.cart_order_count, orderLines.size, orderLines.size |
||||
) |
||||
LazyColumn(modifier) { |
||||
item { |
||||
Spacer(Modifier.windowInsetsTopHeight(getCartContentInsets())) |
||||
Text( |
||||
text = stringResource(MppR.string.cart_order_header, snackCountFormattedString), |
||||
style = MaterialTheme.typography.h6, |
||||
color = JetsnackTheme.colors.brand, |
||||
maxLines = 1, |
||||
overflow = TextOverflow.Ellipsis, |
||||
modifier = Modifier |
||||
.heightIn(min = 56.dp) |
||||
.padding(horizontal = 24.dp, vertical = 4.dp) |
||||
.wrapContentHeight() |
||||
) |
||||
} |
||||
items(orderLines) { orderLine -> |
||||
SwipeDismissItem( |
||||
background = { offsetX -> |
||||
/*Background color changes from light gray to red when the |
||||
swipe to delete with exceeds 160.dp*/ |
||||
val backgroundColor = if (offsetX < -160.dp) { |
||||
JetsnackTheme.colors.error |
||||
} else { |
||||
JetsnackTheme.colors.uiFloated |
||||
} |
||||
Column( |
||||
modifier = Modifier |
||||
.fillMaxWidth() |
||||
.fillMaxHeight() |
||||
.background(backgroundColor), |
||||
horizontalAlignment = Alignment.End, |
||||
verticalArrangement = Arrangement.Center |
||||
) { |
||||
// Set 4.dp padding only if offset is bigger than 160.dp |
||||
val padding: Dp by animateDpAsState( |
||||
if (offsetX > -160.dp) 4.dp else 0.dp |
||||
) |
||||
Box( |
||||
Modifier |
||||
.width(offsetX * -1) |
||||
.padding(padding) |
||||
) { |
||||
// Height equals to width removing padding |
||||
val height = (offsetX + 8.dp) * -1 |
||||
Surface( |
||||
modifier = Modifier |
||||
.fillMaxWidth() |
||||
.height(height) |
||||
.align(Alignment.Center), |
||||
shape = CircleShape, |
||||
color = JetsnackTheme.colors.error |
||||
) { |
||||
Box( |
||||
modifier = Modifier.fillMaxSize(), |
||||
contentAlignment = Alignment.Center |
||||
) { |
||||
// Icon must be visible while in this width range |
||||
if (offsetX < -40.dp && offsetX > -152.dp) { |
||||
// Icon alpha decreases as it is about to disappear |
||||
val iconAlpha: Float by animateFloatAsState( |
||||
if (offsetX < -120.dp) 0.5f else 1f |
||||
) |
||||
|
||||
Icon( |
||||
imageVector = Icons.Filled.DeleteForever, |
||||
modifier = Modifier |
||||
.size(16.dp) |
||||
.graphicsLayer(alpha = iconAlpha), |
||||
tint = JetsnackTheme.colors.uiBackground, |
||||
contentDescription = null, |
||||
) |
||||
} |
||||
/*Text opacity increases as the text is supposed to appear in |
||||
the screen*/ |
||||
val textAlpha by animateFloatAsState( |
||||
if (offsetX > -144.dp) 0.5f else 1f |
||||
) |
||||
if (offsetX < -120.dp) { |
||||
Text( |
||||
text = stringResource(id = MppR.string.remove_item), |
||||
style = MaterialTheme.typography.subtitle1, |
||||
color = JetsnackTheme.colors.uiBackground, |
||||
textAlign = TextAlign.Center, |
||||
modifier = Modifier |
||||
.graphicsLayer( |
||||
alpha = textAlpha |
||||
) |
||||
) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
) { |
||||
ActualCartItem( |
||||
orderLine = orderLine, |
||||
removeSnack = removeSnack, |
||||
increaseItemCount = increaseItemCount, |
||||
decreaseItemCount = decreaseItemCount, |
||||
onSnackClick = onSnackClick |
||||
) |
||||
} |
||||
} |
||||
item { |
||||
SummaryItem( |
||||
subtotal = orderLines.map { it.snack.price * it.count }.sum(), |
||||
shippingCosts = 369 |
||||
) |
||||
} |
||||
item { |
||||
SnackCollection( |
||||
snackCollection = inspiredByCart, |
||||
onSnackClick = onSnackClick, |
||||
highlight = false |
||||
) |
||||
Spacer(Modifier.height(56.dp)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun SummaryItem( |
||||
subtotal: Long, |
||||
shippingCosts: Long, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
Column(modifier) { |
||||
Text( |
||||
text = stringResource(MppR.string.cart_summary_header), |
||||
style = MaterialTheme.typography.h6, |
||||
color = JetsnackTheme.colors.brand, |
||||
maxLines = 1, |
||||
overflow = TextOverflow.Ellipsis, |
||||
modifier = Modifier |
||||
.padding(horizontal = 24.dp) |
||||
.heightIn(min = 56.dp) |
||||
.wrapContentHeight() |
||||
) |
||||
Row(modifier = Modifier.padding(horizontal = 24.dp)) { |
||||
Text( |
||||
text = stringResource(MppR.string.cart_subtotal_label), |
||||
style = MaterialTheme.typography.body1, |
||||
modifier = Modifier |
||||
.weight(1f) |
||||
.wrapContentWidth(Alignment.Start) |
||||
.alignBy(LastBaseline) |
||||
) |
||||
Text( |
||||
text = formatPrice(subtotal), |
||||
style = MaterialTheme.typography.body1, |
||||
modifier = Modifier.alignBy(LastBaseline) |
||||
) |
||||
} |
||||
Row(modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) { |
||||
Text( |
||||
text = stringResource(MppR.string.cart_shipping_label), |
||||
style = MaterialTheme.typography.body1, |
||||
modifier = Modifier |
||||
.weight(1f) |
||||
.wrapContentWidth(Alignment.Start) |
||||
.alignBy(LastBaseline) |
||||
) |
||||
Text( |
||||
text = formatPrice(shippingCosts), |
||||
style = MaterialTheme.typography.body1, |
||||
modifier = Modifier.alignBy(LastBaseline) |
||||
) |
||||
} |
||||
Spacer(modifier = Modifier.height(8.dp)) |
||||
JetsnackDivider() |
||||
Row(modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) { |
||||
Text( |
||||
text = stringResource(MppR.string.cart_total_label), |
||||
style = MaterialTheme.typography.body1, |
||||
modifier = Modifier |
||||
.weight(1f) |
||||
.padding(end = 16.dp) |
||||
.wrapContentWidth(Alignment.End) |
||||
.alignBy(LastBaseline) |
||||
) |
||||
Text( |
||||
text = formatPrice(subtotal + shippingCosts), |
||||
style = MaterialTheme.typography.subtitle1, |
||||
modifier = Modifier.alignBy(LastBaseline) |
||||
) |
||||
} |
||||
JetsnackDivider() |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun CheckoutBar(modifier: Modifier = Modifier) { |
||||
Column( |
||||
modifier.background( |
||||
JetsnackTheme.colors.uiBackground.copy(alpha = AlphaNearOpaque) |
||||
) |
||||
) { |
||||
JetsnackDivider() |
||||
Row { |
||||
Spacer(Modifier.weight(1f)) |
||||
JetsnackButton( |
||||
onClick = { /* todo */ }, |
||||
shape = RectangleShape, |
||||
modifier = Modifier |
||||
.padding(horizontal = 12.dp, vertical = 8.dp) |
||||
.weight(1f) |
||||
) { |
||||
Text( |
||||
text = stringResource(id = MppR.string.cart_checkout), |
||||
modifier = Modifier.fillMaxWidth(), |
||||
textAlign = TextAlign.Left, |
||||
maxLines = 1 |
||||
) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,92 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.home.cart |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.State |
||||
import com.example.jetsnack.* |
||||
import com.example.jetsnack.model.OrderLine |
||||
import com.example.jetsnack.model.SnackRepo |
||||
import com.example.jetsnack.model.SnackbarManager |
||||
import kotlinx.coroutines.flow.MutableStateFlow |
||||
import kotlinx.coroutines.flow.StateFlow |
||||
|
||||
/** |
||||
* Holds the contents of the cart and allows changes to it. |
||||
* |
||||
* TODO: Move data to Repository so it can be displayed and changed consistently throughout the app. |
||||
*/ |
||||
class CartViewModel( |
||||
private val snackbarManager: SnackbarManager, |
||||
snackRepository: SnackRepo |
||||
) : JetSnackCartViewModel() { |
||||
|
||||
private val _orderLines: MutableStateFlow<List<OrderLine>> = |
||||
MutableStateFlow(snackRepository.getCart()) |
||||
val orderLines: StateFlow<List<OrderLine>> get() = _orderLines |
||||
|
||||
// Logic to show errors every few requests |
||||
private var requestCount = 0 |
||||
private fun shouldRandomlyFail(): Boolean = ++requestCount % 5 == 0 |
||||
|
||||
fun increaseSnackCount(snackId: Long) { |
||||
if (!shouldRandomlyFail()) { |
||||
val currentCount = _orderLines.value.first { it.snack.id == snackId }.count |
||||
updateSnackCount(snackId, currentCount + 1) |
||||
} else { |
||||
snackbarManager.showMessage(MppR.string.cart_increase_error) |
||||
} |
||||
} |
||||
|
||||
fun decreaseSnackCount(snackId: Long) { |
||||
if (!shouldRandomlyFail()) { |
||||
val currentCount = _orderLines.value.first { it.snack.id == snackId }.count |
||||
if (currentCount == 1) { |
||||
// remove snack from cart |
||||
removeSnack(snackId) |
||||
} else { |
||||
// update quantity in cart |
||||
updateSnackCount(snackId, currentCount - 1) |
||||
} |
||||
} else { |
||||
snackbarManager.showMessage(MppR.string.cart_decrease_error) |
||||
} |
||||
} |
||||
|
||||
fun removeSnack(snackId: Long) { |
||||
_orderLines.value = _orderLines.value.filter { it.snack.id != snackId } |
||||
} |
||||
|
||||
private fun updateSnackCount(snackId: Long, count: Int) { |
||||
_orderLines.value = _orderLines.value.map { |
||||
if (it.snack.id == snackId) { |
||||
it.copy(count = count) |
||||
} else { |
||||
it |
||||
} |
||||
} |
||||
} |
||||
|
||||
companion object // necessary for android (see `provideFactory` method) |
||||
} |
||||
|
||||
expect abstract class JetSnackCartViewModel() { |
||||
|
||||
@Composable |
||||
fun collectOrderLinesAsState(flow: StateFlow<List<OrderLine>>): State<List<OrderLine>> |
||||
|
||||
} |
@ -0,0 +1,68 @@
|
||||
/* |
||||
* Copyright 2021 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.home.cart |
||||
|
||||
import androidx.compose.animation.AnimatedVisibility |
||||
import androidx.compose.animation.EnterTransition |
||||
import androidx.compose.animation.ExitTransition |
||||
import androidx.compose.animation.ExperimentalAnimationApi |
||||
import androidx.compose.animation.expandVertically |
||||
import androidx.compose.animation.shrinkVertically |
||||
import androidx.compose.material.DismissDirection |
||||
import androidx.compose.material.ExperimentalMaterialApi |
||||
import androidx.compose.material.SwipeToDismiss |
||||
import androidx.compose.material.rememberDismissState |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.platform.LocalDensity |
||||
import androidx.compose.ui.unit.Dp |
||||
|
||||
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class) |
||||
@Composable |
||||
/** |
||||
* Holds the Swipe to dismiss composable, its animation and the current state |
||||
*/ |
||||
fun SwipeDismissItem( |
||||
modifier: Modifier = Modifier, |
||||
directions: Set<DismissDirection> = setOf(DismissDirection.EndToStart), |
||||
enter: EnterTransition = expandVertically(), |
||||
exit: ExitTransition = shrinkVertically(), |
||||
background: @Composable (offset: Dp) -> Unit, |
||||
content: @Composable (isDismissed: Boolean) -> Unit, |
||||
) { |
||||
// Hold the current state from the Swipe to Dismiss composable |
||||
val dismissState = rememberDismissState() |
||||
// Boolean value used for hiding the item if the current state is dismissed |
||||
val isDismissed = dismissState.isDismissed(DismissDirection.EndToStart) |
||||
// Returns the swiped value in dp |
||||
val offset = with(LocalDensity.current) { dismissState.offset.value.toDp() } |
||||
|
||||
AnimatedVisibility( |
||||
modifier = modifier, |
||||
visible = !isDismissed, |
||||
enter = enter, |
||||
exit = exit |
||||
) { |
||||
SwipeToDismiss( |
||||
modifier = modifier, |
||||
state = dismissState, |
||||
directions = directions, |
||||
background = { background(offset) }, |
||||
dismissContent = { content(isDismissed) } |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,166 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.home.search |
||||
|
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.aspectRatio |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.heightIn |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.wrapContentHeight |
||||
import androidx.compose.foundation.lazy.LazyColumn |
||||
import androidx.compose.foundation.lazy.itemsIndexed |
||||
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.runtime.Composable |
||||
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.layout.Layout |
||||
import androidx.compose.ui.unit.Constraints |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.model.SearchCategory |
||||
import com.example.jetsnack.model.SearchCategoryCollection |
||||
import com.example.jetsnack.ui.components.SnackImage |
||||
import com.example.jetsnack.ui.components.VerticalGrid |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
import kotlin.math.max |
||||
|
||||
@Composable |
||||
fun SearchCategories( |
||||
categories: List<SearchCategoryCollection> |
||||
) { |
||||
LazyColumn { |
||||
itemsIndexed(categories) { index, collection -> |
||||
SearchCategoryCollection(collection, index) |
||||
} |
||||
} |
||||
Spacer(Modifier.height(8.dp)) |
||||
} |
||||
|
||||
@Composable |
||||
private fun SearchCategoryCollection( |
||||
collection: SearchCategoryCollection, |
||||
index: Int, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
Column(modifier) { |
||||
Text( |
||||
text = collection.name, |
||||
style = MaterialTheme.typography.h6, |
||||
color = JetsnackTheme.colors.textPrimary, |
||||
modifier = Modifier |
||||
.heightIn(min = 56.dp) |
||||
.padding(horizontal = 24.dp, vertical = 4.dp) |
||||
.wrapContentHeight() |
||||
) |
||||
VerticalGrid(Modifier.padding(horizontal = 16.dp), columns = 2) { |
||||
val gradient = when (index % 2) { |
||||
0 -> JetsnackTheme.colors.gradient2_2 |
||||
else -> JetsnackTheme.colors.gradient2_3 |
||||
} |
||||
collection.categories.forEach { category -> |
||||
SearchCategory( |
||||
category = category, |
||||
gradient = gradient, |
||||
modifier = Modifier.padding(8.dp) |
||||
) |
||||
} |
||||
} |
||||
Spacer(Modifier.height(4.dp)) |
||||
} |
||||
} |
||||
|
||||
private val MinImageSize = 134.dp |
||||
private val CategoryShape = RoundedCornerShape(10.dp) |
||||
private const val CategoryTextProportion = 0.55f |
||||
|
||||
@Composable |
||||
private fun SearchCategory( |
||||
category: SearchCategory, |
||||
gradient: List<Color>, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
Layout( |
||||
modifier = modifier |
||||
.aspectRatio(1.45f) |
||||
.shadow(elevation = 3.dp, shape = CategoryShape) |
||||
.clip(CategoryShape) |
||||
.background(Brush.horizontalGradient(gradient)) |
||||
.clickable { /* todo */ }, |
||||
content = { |
||||
Text( |
||||
text = category.name, |
||||
style = MaterialTheme.typography.subtitle1, |
||||
color = JetsnackTheme.colors.textSecondary, |
||||
modifier = Modifier |
||||
.padding(4.dp) |
||||
.padding(start = 8.dp) |
||||
) |
||||
SnackImage( |
||||
imageUrl = category.imageUrl, |
||||
contentDescription = null, |
||||
modifier = Modifier.fillMaxSize() |
||||
) |
||||
} |
||||
) { measurables, constraints -> |
||||
// Text given a set proportion of width (which is determined by the aspect ratio) |
||||
val textWidth = (constraints.maxWidth * CategoryTextProportion).toInt() |
||||
val textPlaceable = measurables[0].measure(Constraints.fixedWidth(textWidth)) |
||||
|
||||
// Image is sized to the larger of height of item, or a minimum value |
||||
// i.e. may appear larger than item (but clipped to the item bounds) |
||||
val imageSize = max(MinImageSize.roundToPx(), constraints.maxHeight) |
||||
val imagePlaceable = measurables[1].measure(Constraints.fixed(imageSize, imageSize)) |
||||
layout( |
||||
width = constraints.maxWidth, |
||||
height = constraints.minHeight |
||||
) { |
||||
textPlaceable.placeRelative( |
||||
x = 0, |
||||
y = (constraints.maxHeight - textPlaceable.height) / 2 // centered |
||||
) |
||||
imagePlaceable.placeRelative( |
||||
// image is placed to end of text i.e. will overflow to the end (but be clipped) |
||||
x = textWidth, |
||||
y = (constraints.maxHeight - imagePlaceable.height) / 2 // centered |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
//@Preview |
||||
@Composable |
||||
private fun SearchCategoryPreview() { |
||||
JetsnackTheme { |
||||
SearchCategory( |
||||
category = SearchCategory( |
||||
name = "Desserts", |
||||
imageUrl = "" |
||||
), |
||||
gradient = JetsnackTheme.colors.gradient3_2 |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,219 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.home.search |
||||
|
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
import androidx.compose.foundation.Image |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.wrapContentSize |
||||
import androidx.compose.foundation.lazy.LazyColumn |
||||
import androidx.compose.foundation.lazy.itemsIndexed |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.text.style.TextAlign |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.* |
||||
import com.example.jetsnack.model.Filter |
||||
import com.example.jetsnack.model.Snack |
||||
import com.example.jetsnack.model.snacks |
||||
import com.example.jetsnack.ui.components.FilterBar |
||||
import com.example.jetsnack.ui.components.JetsnackSurface |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
@Composable |
||||
fun SearchResults( |
||||
searchResults: List<Snack>, |
||||
filters: List<Filter>, |
||||
onSnackClick: (Long) -> Unit |
||||
) { |
||||
Column { |
||||
FilterBar(filters, onShowFilters = {}) |
||||
Text( |
||||
text = stringResource(MppR.string.search_count, searchResults.size), |
||||
style = MaterialTheme.typography.h6, |
||||
color = JetsnackTheme.colors.textPrimary, |
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp) |
||||
) |
||||
LazyColumn { |
||||
itemsIndexed(searchResults) { index, snack -> |
||||
SearchResult(snack, onSnackClick, index != 0) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun SearchResult( |
||||
snack: Snack, |
||||
onSnackClick: (Long) -> Unit, |
||||
showDivider: Boolean, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
// TODO: implement Search Result (we don't have ConstrainLayout in Compose MPP) |
||||
// ConstraintLayout( |
||||
// modifier = modifier |
||||
// .fillMaxWidth() |
||||
// .clickable { onSnackClick(snack.id) } |
||||
// .padding(horizontal = 24.dp) |
||||
// ) { |
||||
// val (divider, image, name, tag, priceSpacer, price, add) = createRefs() |
||||
// createVerticalChain(name, tag, priceSpacer, price, chainStyle = ChainStyle.Packed) |
||||
// if (showDivider) { |
||||
// JetsnackDivider( |
||||
// Modifier.constrainAs(divider) { |
||||
// linkTo(start = parent.start, end = parent.end) |
||||
// top.linkTo(parent.top) |
||||
// } |
||||
// ) |
||||
// } |
||||
// SnackImage( |
||||
// imageUrl = snack.imageUrl, |
||||
// contentDescription = null, |
||||
// modifier = Modifier |
||||
// .size(100.dp) |
||||
// .constrainAs(image) { |
||||
// linkTo( |
||||
// top = parent.top, |
||||
// topMargin = 16.dp, |
||||
// bottom = parent.bottom, |
||||
// bottomMargin = 16.dp |
||||
// ) |
||||
// start.linkTo(parent.start) |
||||
// } |
||||
// ) |
||||
// Text( |
||||
// text = snack.name, |
||||
// style = MaterialTheme.typography.subtitle1, |
||||
// color = JetsnackTheme.colors.textSecondary, |
||||
// modifier = Modifier.constrainAs(name) { |
||||
// linkTo( |
||||
// start = image.end, |
||||
// startMargin = 16.dp, |
||||
// end = add.start, |
||||
// endMargin = 16.dp, |
||||
// bias = 0f |
||||
// ) |
||||
// } |
||||
// ) |
||||
// Text( |
||||
// text = snack.tagline, |
||||
// style = MaterialTheme.typography.body1, |
||||
// color = JetsnackTheme.colors.textHelp, |
||||
// modifier = Modifier.constrainAs(tag) { |
||||
// linkTo( |
||||
// start = image.end, |
||||
// startMargin = 16.dp, |
||||
// end = add.start, |
||||
// endMargin = 16.dp, |
||||
// bias = 0f |
||||
// ) |
||||
// } |
||||
// ) |
||||
// Spacer( |
||||
// Modifier |
||||
// .height(8.dp) |
||||
// .constrainAs(priceSpacer) { |
||||
// linkTo(top = tag.bottom, bottom = price.top) |
||||
// } |
||||
// ) |
||||
// Text( |
||||
// text = formatPrice(snack.price), |
||||
// style = MaterialTheme.typography.subtitle1, |
||||
// color = JetsnackTheme.colors.textPrimary, |
||||
// modifier = Modifier.constrainAs(price) { |
||||
// linkTo( |
||||
// start = image.end, |
||||
// startMargin = 16.dp, |
||||
// end = add.start, |
||||
// endMargin = 16.dp, |
||||
// bias = 0f |
||||
// ) |
||||
// } |
||||
// ) |
||||
// JetsnackButton( |
||||
// onClick = { /* todo */ }, |
||||
// shape = CircleShape, |
||||
// contentPadding = PaddingValues(0.dp), |
||||
// modifier = Modifier |
||||
// .size(36.dp) |
||||
// .constrainAs(add) { |
||||
// linkTo(top = parent.top, bottom = parent.bottom) |
||||
// end.linkTo(parent.end) |
||||
// } |
||||
// ) { |
||||
// Icon( |
||||
// imageVector = Icons.Outlined.Add, |
||||
// contentDescription = stringResource(R.string.label_add) |
||||
// ) |
||||
// } |
||||
// } |
||||
} |
||||
|
||||
@Composable |
||||
fun NoResults( |
||||
query: String, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
Column( |
||||
horizontalAlignment = Alignment.CenterHorizontally, |
||||
modifier = modifier |
||||
.fillMaxSize() |
||||
.wrapContentSize() |
||||
.padding(24.dp) |
||||
) { |
||||
Image( |
||||
painterResource(MppR.drawable.empty_state_search), |
||||
contentDescription = null |
||||
) |
||||
Spacer(Modifier.height(24.dp)) |
||||
Text( |
||||
text = stringResource(MppR.string.search_no_matches, query), |
||||
style = MaterialTheme.typography.subtitle1, |
||||
textAlign = TextAlign.Center, |
||||
modifier = Modifier.fillMaxWidth() |
||||
) |
||||
Spacer(Modifier.height(16.dp)) |
||||
Text( |
||||
text = stringResource(MppR.string.search_no_matches_retry), |
||||
style = MaterialTheme.typography.body2, |
||||
textAlign = TextAlign.Center, |
||||
modifier = Modifier.fillMaxWidth() |
||||
) |
||||
} |
||||
} |
||||
|
||||
//@Preview |
||||
@Composable |
||||
private fun SearchResultPreview() { |
||||
JetsnackTheme { |
||||
JetsnackSurface { |
||||
SearchResult( |
||||
snack = snacks[0], |
||||
onSnackClick = { }, |
||||
showDivider = false |
||||
) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,259 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.home.search |
||||
|
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
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.wrapContentHeight |
||||
import androidx.compose.foundation.layout.wrapContentSize |
||||
import androidx.compose.foundation.text.BasicTextField |
||||
import androidx.compose.material.CircularProgressIndicator |
||||
import androidx.compose.material.Icon |
||||
import androidx.compose.material.IconButton |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.outlined.Search |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.LaunchedEffect |
||||
import androidx.compose.runtime.Stable |
||||
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.focus.onFocusChanged |
||||
import androidx.compose.ui.text.input.TextFieldValue |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.* |
||||
import com.example.jetsnack.model.Filter |
||||
import com.example.jetsnack.model.SearchCategoryCollection |
||||
import com.example.jetsnack.model.SearchRepo |
||||
import com.example.jetsnack.model.SearchSuggestionGroup |
||||
import com.example.jetsnack.model.Snack |
||||
import com.example.jetsnack.model.SnackRepo |
||||
import com.example.jetsnack.ui.components.JetsnackDivider |
||||
import com.example.jetsnack.ui.components.JetsnackSurface |
||||
import com.example.jetsnack.ui.snackdetail.jetSnackStatusBarsPadding |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
import com.example.jetsnack.ui.utils.mirroringBackIcon |
||||
|
||||
@Composable |
||||
fun Search( |
||||
onSnackClick: (Long) -> Unit, |
||||
modifier: Modifier = Modifier, |
||||
state: SearchState = rememberSearchState() |
||||
) { |
||||
JetsnackSurface(modifier = modifier.fillMaxSize()) { |
||||
Column { |
||||
Spacer(modifier = Modifier.jetSnackStatusBarsPadding()) |
||||
SearchBar( |
||||
query = state.query, |
||||
onQueryChange = { state.query = it }, |
||||
searchFocused = state.focused, |
||||
onSearchFocusChange = { state.focused = it }, |
||||
onClearQuery = { state.query = TextFieldValue("") }, |
||||
searching = state.searching |
||||
) |
||||
JetsnackDivider() |
||||
|
||||
LaunchedEffect(state.query.text) { |
||||
state.searching = true |
||||
state.searchResults = SearchRepo.search(state.query.text) |
||||
state.searching = false |
||||
} |
||||
when (state.searchDisplay) { |
||||
SearchDisplay.Categories -> SearchCategories(state.categories) |
||||
SearchDisplay.Suggestions -> SearchSuggestions( |
||||
suggestions = state.suggestions, |
||||
onSuggestionSelect = { suggestion -> state.query = TextFieldValue(suggestion) } |
||||
) |
||||
SearchDisplay.Results -> SearchResults( |
||||
state.searchResults, |
||||
state.filters, |
||||
onSnackClick |
||||
) |
||||
SearchDisplay.NoResults -> NoResults(state.query.text) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
enum class SearchDisplay { |
||||
Categories, Suggestions, Results, NoResults |
||||
} |
||||
|
||||
@Composable |
||||
private fun rememberSearchState( |
||||
query: TextFieldValue = TextFieldValue(""), |
||||
focused: Boolean = false, |
||||
searching: Boolean = false, |
||||
categories: List<SearchCategoryCollection> = SearchRepo.getCategories(), |
||||
suggestions: List<SearchSuggestionGroup> = SearchRepo.getSuggestions(), |
||||
filters: List<Filter> = SnackRepo.getFilters(), |
||||
searchResults: List<Snack> = emptyList() |
||||
): SearchState { |
||||
return remember { |
||||
SearchState( |
||||
query = query, |
||||
focused = focused, |
||||
searching = searching, |
||||
categories = categories, |
||||
suggestions = suggestions, |
||||
filters = filters, |
||||
searchResults = searchResults |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Stable |
||||
class SearchState( |
||||
query: TextFieldValue, |
||||
focused: Boolean, |
||||
searching: Boolean, |
||||
categories: List<SearchCategoryCollection>, |
||||
suggestions: List<SearchSuggestionGroup>, |
||||
filters: List<Filter>, |
||||
searchResults: List<Snack> |
||||
) { |
||||
var query by mutableStateOf(query) |
||||
var focused by mutableStateOf(focused) |
||||
var searching by mutableStateOf(searching) |
||||
var categories by mutableStateOf(categories) |
||||
var suggestions by mutableStateOf(suggestions) |
||||
var filters by mutableStateOf(filters) |
||||
var searchResults by mutableStateOf(searchResults) |
||||
val searchDisplay: SearchDisplay |
||||
get() = when { |
||||
!focused && query.text.isEmpty() -> SearchDisplay.Categories |
||||
focused && query.text.isEmpty() -> SearchDisplay.Suggestions |
||||
searchResults.isEmpty() -> SearchDisplay.NoResults |
||||
else -> SearchDisplay.Results |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun SearchBar( |
||||
query: TextFieldValue, |
||||
onQueryChange: (TextFieldValue) -> Unit, |
||||
searchFocused: Boolean, |
||||
onSearchFocusChange: (Boolean) -> Unit, |
||||
onClearQuery: () -> Unit, |
||||
searching: Boolean, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
JetsnackSurface( |
||||
color = JetsnackTheme.colors.uiFloated, |
||||
contentColor = JetsnackTheme.colors.textSecondary, |
||||
shape = MaterialTheme.shapes.small, |
||||
modifier = modifier |
||||
.fillMaxWidth() |
||||
.height(56.dp) |
||||
.padding(horizontal = 24.dp, vertical = 8.dp) |
||||
) { |
||||
Box(Modifier.fillMaxSize()) { |
||||
if (query.text.isEmpty()) { |
||||
SearchHint() |
||||
} |
||||
Row( |
||||
verticalAlignment = Alignment.CenterVertically, |
||||
modifier = Modifier |
||||
.fillMaxSize() |
||||
.wrapContentHeight() |
||||
) { |
||||
if (searchFocused) { |
||||
IconButton(onClick = onClearQuery) { |
||||
Icon( |
||||
imageVector = mirroringBackIcon(), |
||||
tint = JetsnackTheme.colors.iconPrimary, |
||||
contentDescription = stringResource(MppR.string.label_back) |
||||
) |
||||
} |
||||
} |
||||
BasicTextField( |
||||
value = query, |
||||
onValueChange = onQueryChange, |
||||
modifier = Modifier |
||||
.weight(1f) |
||||
.onFocusChanged { |
||||
onSearchFocusChange(it.isFocused) |
||||
} |
||||
) |
||||
if (searching) { |
||||
CircularProgressIndicator( |
||||
color = JetsnackTheme.colors.iconPrimary, |
||||
modifier = Modifier |
||||
.padding(horizontal = 6.dp) |
||||
.size(36.dp) |
||||
) |
||||
} else { |
||||
Spacer(Modifier.width(IconSize)) // balance arrow icon |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private val IconSize = 48.dp |
||||
|
||||
@Composable |
||||
private fun SearchHint() { |
||||
Row( |
||||
verticalAlignment = Alignment.CenterVertically, |
||||
modifier = Modifier |
||||
.fillMaxSize() |
||||
.wrapContentSize() |
||||
) { |
||||
Icon( |
||||
imageVector = Icons.Outlined.Search, |
||||
tint = JetsnackTheme.colors.textHelp, |
||||
contentDescription = stringResource(MppR.string.label_search) |
||||
) |
||||
Spacer(Modifier.width(8.dp)) |
||||
Text( |
||||
text = stringResource(MppR.string.search_jetsnack), |
||||
color = JetsnackTheme.colors.textHelp |
||||
) |
||||
} |
||||
} |
||||
|
||||
//@Preview |
||||
@Composable |
||||
private fun SearchBarPreview() { |
||||
JetsnackTheme { |
||||
JetsnackSurface { |
||||
SearchBar( |
||||
query = TextFieldValue(""), |
||||
onQueryChange = { }, |
||||
searchFocused = false, |
||||
onSearchFocusChange = { }, |
||||
onClearQuery = { }, |
||||
searching = false |
||||
) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,108 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.home.search |
||||
|
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.heightIn |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.wrapContentHeight |
||||
import androidx.compose.foundation.layout.wrapContentSize |
||||
import androidx.compose.foundation.lazy.LazyColumn |
||||
import androidx.compose.foundation.lazy.items |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.model.SearchRepo |
||||
import com.example.jetsnack.model.SearchSuggestionGroup |
||||
import com.example.jetsnack.ui.components.JetsnackSurface |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
|
||||
@Composable |
||||
fun SearchSuggestions( |
||||
suggestions: List<SearchSuggestionGroup>, |
||||
onSuggestionSelect: (String) -> Unit |
||||
) { |
||||
LazyColumn { |
||||
suggestions.forEach { suggestionGroup -> |
||||
item { |
||||
SuggestionHeader(suggestionGroup.name) |
||||
} |
||||
items(suggestionGroup.suggestions) { suggestion -> |
||||
Suggestion( |
||||
suggestion = suggestion, |
||||
onSuggestionSelect = onSuggestionSelect, |
||||
modifier = Modifier.fillParentMaxWidth() |
||||
) |
||||
} |
||||
item { |
||||
Spacer(Modifier.height(4.dp)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun SuggestionHeader( |
||||
name: String, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
Text( |
||||
text = name, |
||||
style = MaterialTheme.typography.h6, |
||||
color = JetsnackTheme.colors.textPrimary, |
||||
modifier = modifier |
||||
.heightIn(min = 56.dp) |
||||
.padding(horizontal = 24.dp, vertical = 4.dp) |
||||
.wrapContentHeight() |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
private fun Suggestion( |
||||
suggestion: String, |
||||
onSuggestionSelect: (String) -> Unit, |
||||
modifier: Modifier = Modifier |
||||
) { |
||||
Text( |
||||
text = suggestion, |
||||
style = MaterialTheme.typography.subtitle1, |
||||
modifier = modifier |
||||
.heightIn(min = 48.dp) |
||||
.clickable { onSuggestionSelect(suggestion) } |
||||
.padding(start = 24.dp) |
||||
.wrapContentSize(Alignment.CenterStart) |
||||
) |
||||
} |
||||
|
||||
//@Preview |
||||
@Composable |
||||
fun PreviewSuggestions() { |
||||
JetsnackTheme { |
||||
JetsnackSurface { |
||||
SearchSuggestions( |
||||
suggestions = SearchRepo.getSuggestions(), |
||||
onSuggestionSelect = { } |
||||
) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,406 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.snackdetail |
||||
|
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
import androidx.compose.foundation.ScrollState |
||||
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.heightIn |
||||
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.rememberScrollState |
||||
import androidx.compose.foundation.shape.CircleShape |
||||
import androidx.compose.foundation.verticalScroll |
||||
import androidx.compose.material.Icon |
||||
import androidx.compose.material.IconButton |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.key |
||||
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.layout.Layout |
||||
import androidx.compose.ui.platform.LocalDensity |
||||
import androidx.compose.ui.text.style.TextAlign |
||||
import androidx.compose.ui.text.style.TextOverflow |
||||
import androidx.compose.ui.unit.Constraints |
||||
import androidx.compose.ui.unit.IntOffset |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.compose.ui.unit.lerp |
||||
import androidx.compose.ui.unit.sp |
||||
import com.example.jetsnack.* |
||||
import com.example.jetsnack.model.Snack |
||||
import com.example.jetsnack.model.SnackCollection |
||||
import com.example.jetsnack.model.SnackRepo |
||||
import com.example.jetsnack.ui.components.JetsnackButton |
||||
import com.example.jetsnack.ui.components.JetsnackDivider |
||||
import com.example.jetsnack.ui.components.JetsnackSurface |
||||
import com.example.jetsnack.ui.components.QuantitySelector |
||||
import com.example.jetsnack.ui.components.SnackCollection |
||||
import com.example.jetsnack.ui.components.SnackImage |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
import com.example.jetsnack.ui.theme.Neutral8 |
||||
import com.example.jetsnack.ui.utils.formatPrice |
||||
import com.example.jetsnack.ui.utils.mirroringBackIcon |
||||
import kotlin.math.max |
||||
import kotlin.math.min |
||||
import kotlin.math.roundToInt |
||||
import kotlin.math.roundToLong |
||||
|
||||
private val BottomBarHeight = 56.dp |
||||
private val TitleHeight = 128.dp |
||||
private val GradientScroll = 180.dp |
||||
private val ImageOverlap = 115.dp |
||||
private val MinTitleOffset = 56.dp |
||||
private val MinImageOffset = 12.dp |
||||
private val MaxTitleOffset = ImageOverlap + MinTitleOffset + GradientScroll |
||||
private val ExpandedImageSize = 300.dp |
||||
private val CollapsedImageSize = 150.dp |
||||
private val HzPadding = Modifier.padding(horizontal = 24.dp) |
||||
|
||||
@Composable |
||||
fun SnackDetail( |
||||
snackId: Long, |
||||
upPress: () -> Unit, |
||||
onSnackClick: (Long) -> Unit, |
||||
) { |
||||
val snack = remember(snackId) { SnackRepo.getSnack(snackId) } |
||||
val related = remember(snackId) { SnackRepo.getRelated(snackId) } |
||||
|
||||
Box(Modifier.fillMaxSize()) { |
||||
val scroll = rememberScrollState(0) |
||||
Header() |
||||
Body(related, scroll, onSnackClick) |
||||
Title(snack) { scroll.value } |
||||
Image(snack.imageUrl) { scroll.value } |
||||
Up(upPress) |
||||
CartBottomBar(modifier = Modifier.align(Alignment.BottomCenter)) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun Header() { |
||||
Spacer( |
||||
modifier = Modifier |
||||
.height(280.dp) |
||||
.fillMaxWidth() |
||||
.background(Brush.horizontalGradient(JetsnackTheme.colors.tornado1)) |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
private fun Up(upPress: () -> Unit) { |
||||
IconButton( |
||||
onClick = upPress, |
||||
modifier = Modifier |
||||
.jetSnackStatusBarsPadding() |
||||
.padding(horizontal = 16.dp, vertical = 10.dp) |
||||
.size(36.dp) |
||||
.background( |
||||
color = Neutral8.copy(alpha = 0.32f), |
||||
shape = CircleShape |
||||
) |
||||
) { |
||||
Icon( |
||||
imageVector = mirroringBackIcon(), |
||||
tint = JetsnackTheme.colors.iconInteractive, |
||||
contentDescription = stringResource(MppR.string.label_back) |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun Body( |
||||
related: List<SnackCollection>, |
||||
scroll: ScrollState, |
||||
onSnackClick: (Long) -> Unit, |
||||
) { |
||||
Column { |
||||
Spacer( |
||||
modifier = Modifier |
||||
.fillMaxWidth() |
||||
.jetSnackStatusBarsPadding() |
||||
.height(MinTitleOffset) |
||||
) |
||||
Column( |
||||
modifier = Modifier.verticalScroll(scroll) |
||||
) { |
||||
Spacer(Modifier.height(GradientScroll)) |
||||
JetsnackSurface(Modifier.fillMaxWidth()) { |
||||
Column { |
||||
Spacer(Modifier.height(ImageOverlap)) |
||||
Spacer(Modifier.height(TitleHeight)) |
||||
|
||||
Spacer(Modifier.height(16.dp)) |
||||
Text( |
||||
text = stringResource(MppR.string.detail_header), |
||||
style = MaterialTheme.typography.overline, |
||||
color = JetsnackTheme.colors.textHelp, |
||||
modifier = HzPadding |
||||
) |
||||
Spacer(Modifier.height(16.dp)) |
||||
var seeMore by remember { mutableStateOf(true) } |
||||
Text( |
||||
text = stringResource(MppR.string.detail_placeholder), |
||||
style = MaterialTheme.typography.body1, |
||||
color = JetsnackTheme.colors.textHelp, |
||||
maxLines = if (seeMore) 5 else Int.MAX_VALUE, |
||||
overflow = TextOverflow.Ellipsis, |
||||
modifier = HzPadding |
||||
) |
||||
val textButton = if (seeMore) { |
||||
stringResource(id = MppR.string.see_more) |
||||
} else { |
||||
stringResource(id = MppR.string.see_less) |
||||
} |
||||
Text( |
||||
text = textButton, |
||||
style = MaterialTheme.typography.button, |
||||
textAlign = TextAlign.Center, |
||||
color = JetsnackTheme.colors.textLink, |
||||
modifier = Modifier |
||||
.heightIn(20.dp) |
||||
.fillMaxWidth() |
||||
.padding(top = 15.dp) |
||||
.clickable { |
||||
seeMore = !seeMore |
||||
} |
||||
) |
||||
Spacer(Modifier.height(40.dp)) |
||||
Text( |
||||
text = stringResource(MppR.string.ingredients), |
||||
style = MaterialTheme.typography.overline, |
||||
color = JetsnackTheme.colors.textHelp, |
||||
modifier = HzPadding |
||||
) |
||||
Spacer(Modifier.height(4.dp)) |
||||
Text( |
||||
text = stringResource(MppR.string.ingredients_list), |
||||
style = MaterialTheme.typography.body1, |
||||
color = JetsnackTheme.colors.textHelp, |
||||
modifier = HzPadding |
||||
) |
||||
|
||||
Spacer(Modifier.height(16.dp)) |
||||
JetsnackDivider() |
||||
|
||||
related.forEach { snackCollection -> |
||||
key(snackCollection.id) { |
||||
SnackCollection( |
||||
snackCollection = snackCollection, |
||||
onSnackClick = onSnackClick, |
||||
highlight = false |
||||
) |
||||
} |
||||
} |
||||
|
||||
Spacer( |
||||
modifier = Modifier |
||||
.padding(bottom = BottomBarHeight) |
||||
.jetSnackNavigationBarsPadding() |
||||
.height(8.dp) |
||||
) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun Title(snack: Snack, scrollProvider: () -> Int) { |
||||
val maxOffset = with(LocalDensity.current) { MaxTitleOffset.toPx() } |
||||
val minOffset = with(LocalDensity.current) { MinTitleOffset.toPx() } |
||||
|
||||
Column( |
||||
verticalArrangement = Arrangement.Bottom, |
||||
modifier = Modifier |
||||
.heightIn(min = TitleHeight) |
||||
.jetSnackStatusBarsPadding() |
||||
.offset { |
||||
val scroll = scrollProvider() |
||||
val offset = (maxOffset - scroll).coerceAtLeast(minOffset) |
||||
IntOffset(x = 0, y = offset.toInt()) |
||||
} |
||||
.background(color = JetsnackTheme.colors.uiBackground) |
||||
) { |
||||
Spacer(Modifier.height(16.dp)) |
||||
Text( |
||||
text = snack.name, |
||||
style = MaterialTheme.typography.h4, |
||||
color = JetsnackTheme.colors.textSecondary, |
||||
modifier = HzPadding |
||||
) |
||||
Text( |
||||
text = snack.tagline, |
||||
style = MaterialTheme.typography.subtitle2, |
||||
fontSize = 20.sp, |
||||
color = JetsnackTheme.colors.textHelp, |
||||
modifier = HzPadding |
||||
) |
||||
Spacer(Modifier.height(4.dp)) |
||||
Text( |
||||
text = formatPrice(snack.price), |
||||
style = MaterialTheme.typography.h6, |
||||
color = JetsnackTheme.colors.textPrimary, |
||||
modifier = HzPadding |
||||
) |
||||
|
||||
Spacer(Modifier.height(8.dp)) |
||||
JetsnackDivider() |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun Image( |
||||
imageUrl: String, |
||||
scrollProvider: () -> Int |
||||
) { |
||||
val collapseRange = with(LocalDensity.current) { (MaxTitleOffset - MinTitleOffset).toPx() } |
||||
val collapseFractionProvider = { |
||||
(scrollProvider() / collapseRange).coerceIn(0f, 1f) |
||||
} |
||||
|
||||
CollapsingImageLayout( |
||||
collapseFractionProvider = collapseFractionProvider, |
||||
modifier = HzPadding.then(Modifier.jetSnackStatusBarsPadding()) |
||||
) { |
||||
SnackImage( |
||||
imageUrl = imageUrl, |
||||
contentDescription = null, |
||||
modifier = Modifier.fillMaxSize() |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun CollapsingImageLayout( |
||||
collapseFractionProvider: () -> Float, |
||||
modifier: Modifier = Modifier, |
||||
content: @Composable () -> Unit |
||||
) { |
||||
Layout( |
||||
modifier = modifier, |
||||
content = content |
||||
) { measurables, constraints -> |
||||
check(measurables.size == 1) |
||||
|
||||
val collapseFraction = collapseFractionProvider() |
||||
|
||||
val imageMaxSize = min(ExpandedImageSize.roundToPx(), constraints.maxWidth) |
||||
val imageMinSize = max(CollapsedImageSize.roundToPx(), constraints.minWidth) |
||||
val imageWidth = lerp(imageMaxSize, imageMinSize, collapseFraction) |
||||
val imagePlaceable = measurables[0].measure(Constraints.fixed(imageWidth, imageWidth)) |
||||
|
||||
val imageY = lerp(MinTitleOffset, MinImageOffset, collapseFraction).roundToPx() |
||||
val imageX = lerp( |
||||
(constraints.maxWidth - imageWidth) / 2, // centered when expanded |
||||
constraints.maxWidth - imageWidth, // right aligned when collapsed |
||||
collapseFraction |
||||
) |
||||
layout( |
||||
width = constraints.maxWidth, |
||||
height = imageY + imageWidth |
||||
) { |
||||
imagePlaceable.placeRelative(imageX, imageY) |
||||
} |
||||
} |
||||
} |
||||
|
||||
fun lerp(start: Float, stop: Float, fraction: Float): Float { |
||||
return (1 - fraction) * start + fraction * stop |
||||
} |
||||
|
||||
/** |
||||
* Linearly interpolate between [start] and [stop] with [fraction] fraction between them. |
||||
*/ |
||||
fun lerp(start: Int, stop: Int, fraction: Float): Int { |
||||
return start + ((stop - start) * fraction.toDouble()).roundToInt() |
||||
} |
||||
|
||||
/** |
||||
* Linearly interpolate between [start] and [stop] with [fraction] fraction between them. |
||||
*/ |
||||
fun lerp(start: Long, stop: Long, fraction: Float): Long { |
||||
return start + ((stop - start) * fraction.toDouble()).roundToLong() |
||||
} |
||||
|
||||
@Composable |
||||
private fun CartBottomBar(modifier: Modifier = Modifier) { |
||||
val (count, updateCount) = remember { mutableStateOf(1) } |
||||
JetsnackSurface(modifier) { |
||||
Column { |
||||
JetsnackDivider() |
||||
Row( |
||||
verticalAlignment = Alignment.CenterVertically, |
||||
modifier = Modifier |
||||
.jetSnackNavigationBarsPadding() |
||||
.then(HzPadding) |
||||
.heightIn(min = BottomBarHeight) |
||||
) { |
||||
QuantitySelector( |
||||
count = count, |
||||
decreaseItemCount = { if (count > 0) updateCount(count - 1) }, |
||||
increaseItemCount = { updateCount(count + 1) } |
||||
) |
||||
Spacer(Modifier.width(16.dp)) |
||||
JetsnackButton( |
||||
onClick = { /* todo */ }, |
||||
modifier = Modifier.weight(1f) |
||||
) { |
||||
Text( |
||||
text = stringResource(MppR.string.add_to_cart), |
||||
modifier = Modifier.fillMaxWidth(), |
||||
textAlign = TextAlign.Center, |
||||
maxLines = 1 |
||||
) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
expect fun Modifier.jetSnackNavigationBarsPadding(): Modifier |
||||
expect fun Modifier.jetSnackStatusBarsPadding(): Modifier |
||||
expect fun Modifier.jetSnackSystemBarsPadding(): Modifier |
||||
|
||||
//@Preview |
||||
@Composable |
||||
private fun SnackDetailPreview() { |
||||
JetsnackTheme { |
||||
SnackDetail( |
||||
snackId = 1L, |
||||
upPress = { }, |
||||
onSnackClick = { } |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,89 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.theme |
||||
|
||||
import androidx.compose.ui.graphics.Color |
||||
|
||||
val Shadow11 = Color(0xff001787) |
||||
val Shadow10 = Color(0xff00119e) |
||||
val Shadow9 = Color(0xff0009b3) |
||||
val Shadow8 = Color(0xff0200c7) |
||||
val Shadow7 = Color(0xff0e00d7) |
||||
val Shadow6 = Color(0xff2a13e4) |
||||
val Shadow5 = Color(0xff4b30ed) |
||||
val Shadow4 = Color(0xff7057f5) |
||||
val Shadow3 = Color(0xff9b86fa) |
||||
val Shadow2 = Color(0xffc8bbfd) |
||||
val Shadow1 = Color(0xffded6fe) |
||||
val Shadow0 = Color(0xfff4f2ff) |
||||
|
||||
val Ocean11 = Color(0xff005687) |
||||
val Ocean10 = Color(0xff006d9e) |
||||
val Ocean9 = Color(0xff0087b3) |
||||
val Ocean8 = Color(0xff00a1c7) |
||||
val Ocean7 = Color(0xff00b9d7) |
||||
val Ocean6 = Color(0xff13d0e4) |
||||
val Ocean5 = Color(0xff30e2ed) |
||||
val Ocean4 = Color(0xff57eff5) |
||||
val Ocean3 = Color(0xff86f7fa) |
||||
val Ocean2 = Color(0xffbbfdfd) |
||||
val Ocean1 = Color(0xffd6fefe) |
||||
val Ocean0 = Color(0xfff2ffff) |
||||
|
||||
val Lavender11 = Color(0xff170085) |
||||
val Lavender10 = Color(0xff23009e) |
||||
val Lavender9 = Color(0xff3300b3) |
||||
val Lavender8 = Color(0xff4400c7) |
||||
val Lavender7 = Color(0xff5500d7) |
||||
val Lavender6 = Color(0xff6f13e4) |
||||
val Lavender5 = Color(0xff8a30ed) |
||||
val Lavender4 = Color(0xffa557f5) |
||||
val Lavender3 = Color(0xffc186fa) |
||||
val Lavender2 = Color(0xffdebbfd) |
||||
val Lavender1 = Color(0xffebd6fe) |
||||
val Lavender0 = Color(0xfff9f2ff) |
||||
|
||||
val Rose11 = Color(0xff7f0054) |
||||
val Rose10 = Color(0xff97005c) |
||||
val Rose9 = Color(0xffaf0060) |
||||
val Rose8 = Color(0xffc30060) |
||||
val Rose7 = Color(0xffd4005d) |
||||
val Rose6 = Color(0xffe21365) |
||||
val Rose5 = Color(0xffec3074) |
||||
val Rose4 = Color(0xfff4568b) |
||||
val Rose3 = Color(0xfff985aa) |
||||
val Rose2 = Color(0xfffdbbcf) |
||||
val Rose1 = Color(0xfffed6e2) |
||||
val Rose0 = Color(0xfffff2f6) |
||||
|
||||
val Neutral8 = Color(0xff121212) |
||||
val Neutral7 = Color(0xde000000) |
||||
val Neutral6 = Color(0x99000000) |
||||
val Neutral5 = Color(0x61000000) |
||||
val Neutral4 = Color(0x1f000000) |
||||
val Neutral3 = Color(0x1fffffff) |
||||
val Neutral2 = Color(0x61ffffff) |
||||
val Neutral1 = Color(0xbdffffff) |
||||
val Neutral0 = Color(0xffffffff) |
||||
|
||||
val FunctionalRed = Color(0xffd00036) |
||||
val FunctionalRedDark = Color(0xffea6d7e) |
||||
val FunctionalGreen = Color(0xff52c41a) |
||||
val FunctionalGrey = Color(0xfff6f6f6) |
||||
val FunctionalDarkGrey = Color(0xff2e2e2e) |
||||
|
||||
const val AlphaNearOpaque = 0.95f |
@ -0,0 +1,27 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.theme |
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
import androidx.compose.material.Shapes |
||||
import androidx.compose.ui.unit.dp |
||||
|
||||
val Shapes = Shapes( |
||||
small = RoundedCornerShape(percent = 50), |
||||
medium = RoundedCornerShape(20.dp), |
||||
large = RoundedCornerShape(0.dp) |
||||
) |
@ -0,0 +1,310 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.theme |
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme |
||||
import androidx.compose.material.Colors |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.CompositionLocalProvider |
||||
import androidx.compose.runtime.SideEffect |
||||
import androidx.compose.runtime.Stable |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.mutableStateOf |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.setValue |
||||
import androidx.compose.runtime.staticCompositionLocalOf |
||||
import androidx.compose.ui.graphics.Color |
||||
//import com.google.accompanist.systemuicontroller.rememberSystemUiController |
||||
|
||||
private val LightColorPalette = JetsnackColors( |
||||
brand = Shadow5, |
||||
brandSecondary = Ocean3, |
||||
uiBackground = Neutral0, |
||||
uiBorder = Neutral4, |
||||
uiFloated = FunctionalGrey, |
||||
textSecondary = Neutral7, |
||||
textHelp = Neutral6, |
||||
textInteractive = Neutral0, |
||||
textLink = Ocean11, |
||||
iconSecondary = Neutral7, |
||||
iconInteractive = Neutral0, |
||||
iconInteractiveInactive = Neutral1, |
||||
error = FunctionalRed, |
||||
gradient6_1 = listOf(Shadow4, Ocean3, Shadow2, Ocean3, Shadow4), |
||||
gradient6_2 = listOf(Rose4, Lavender3, Rose2, Lavender3, Rose4), |
||||
gradient3_1 = listOf(Shadow2, Ocean3, Shadow4), |
||||
gradient3_2 = listOf(Rose2, Lavender3, Rose4), |
||||
gradient2_1 = listOf(Shadow4, Shadow11), |
||||
gradient2_2 = listOf(Ocean3, Shadow3), |
||||
gradient2_3 = listOf(Lavender3, Rose2), |
||||
tornado1 = listOf(Shadow4, Ocean3), |
||||
isDark = false |
||||
) |
||||
|
||||
private val DarkColorPalette = JetsnackColors( |
||||
brand = Shadow1, |
||||
brandSecondary = Ocean2, |
||||
uiBackground = Neutral8, |
||||
uiBorder = Neutral3, |
||||
uiFloated = FunctionalDarkGrey, |
||||
textPrimary = Shadow1, |
||||
textSecondary = Neutral0, |
||||
textHelp = Neutral1, |
||||
textInteractive = Neutral7, |
||||
textLink = Ocean2, |
||||
iconPrimary = Shadow1, |
||||
iconSecondary = Neutral0, |
||||
iconInteractive = Neutral7, |
||||
iconInteractiveInactive = Neutral6, |
||||
error = FunctionalRedDark, |
||||
gradient6_1 = listOf(Shadow5, Ocean7, Shadow9, Ocean7, Shadow5), |
||||
gradient6_2 = listOf(Rose11, Lavender7, Rose8, Lavender7, Rose11), |
||||
gradient3_1 = listOf(Shadow9, Ocean7, Shadow5), |
||||
gradient3_2 = listOf(Rose8, Lavender7, Rose11), |
||||
gradient2_1 = listOf(Ocean3, Shadow3), |
||||
gradient2_2 = listOf(Ocean4, Shadow2), |
||||
gradient2_3 = listOf(Lavender3, Rose3), |
||||
tornado1 = listOf(Shadow4, Ocean3), |
||||
isDark = true |
||||
) |
||||
|
||||
@Composable |
||||
fun JetsnackTheme( |
||||
darkTheme: Boolean = isSystemInDarkTheme(), |
||||
content: @Composable () -> Unit |
||||
) { |
||||
val colors = if (darkTheme) DarkColorPalette else LightColorPalette |
||||
|
||||
// TODO: implement setSystemBarsColor for android! |
||||
// val sysUiController = rememberSystemUiController() |
||||
// SideEffect { |
||||
// sysUiController.setSystemBarsColor( |
||||
// color = colors.uiBackground.copy(alpha = AlphaNearOpaque) |
||||
// ) |
||||
// } |
||||
|
||||
ProvideJetsnackColors(colors) { |
||||
MaterialTheme( |
||||
colors = debugColors(darkTheme), |
||||
typography = Typography, |
||||
shapes = Shapes, |
||||
content = content |
||||
) |
||||
} |
||||
} |
||||
|
||||
object JetsnackTheme { |
||||
val colors: JetsnackColors |
||||
@Composable |
||||
get() = LocalJetsnackColors.current |
||||
} |
||||
|
||||
/** |
||||
* Jetsnack custom Color Palette |
||||
*/ |
||||
@Stable |
||||
class JetsnackColors( |
||||
gradient6_1: List<Color>, |
||||
gradient6_2: List<Color>, |
||||
gradient3_1: List<Color>, |
||||
gradient3_2: List<Color>, |
||||
gradient2_1: List<Color>, |
||||
gradient2_2: List<Color>, |
||||
gradient2_3: List<Color>, |
||||
brand: Color, |
||||
brandSecondary: Color, |
||||
uiBackground: Color, |
||||
uiBorder: Color, |
||||
uiFloated: Color, |
||||
interactivePrimary: List<Color> = gradient2_1, |
||||
interactiveSecondary: List<Color> = gradient2_2, |
||||
interactiveMask: List<Color> = gradient6_1, |
||||
textPrimary: Color = brand, |
||||
textSecondary: Color, |
||||
textHelp: Color, |
||||
textInteractive: Color, |
||||
textLink: Color, |
||||
tornado1: List<Color>, |
||||
iconPrimary: Color = brand, |
||||
iconSecondary: Color, |
||||
iconInteractive: Color, |
||||
iconInteractiveInactive: Color, |
||||
error: Color, |
||||
notificationBadge: Color = error, |
||||
isDark: Boolean |
||||
) { |
||||
var gradient6_1 by mutableStateOf(gradient6_1) |
||||
private set |
||||
var gradient6_2 by mutableStateOf(gradient6_2) |
||||
private set |
||||
var gradient3_1 by mutableStateOf(gradient3_1) |
||||
private set |
||||
var gradient3_2 by mutableStateOf(gradient3_2) |
||||
private set |
||||
var gradient2_1 by mutableStateOf(gradient2_1) |
||||
private set |
||||
var gradient2_2 by mutableStateOf(gradient2_2) |
||||
private set |
||||
var gradient2_3 by mutableStateOf(gradient2_3) |
||||
private set |
||||
var brand by mutableStateOf(brand) |
||||
private set |
||||
var brandSecondary by mutableStateOf(brandSecondary) |
||||
private set |
||||
var uiBackground by mutableStateOf(uiBackground) |
||||
private set |
||||
var uiBorder by mutableStateOf(uiBorder) |
||||
private set |
||||
var uiFloated by mutableStateOf(uiFloated) |
||||
private set |
||||
var interactivePrimary by mutableStateOf(interactivePrimary) |
||||
private set |
||||
var interactiveSecondary by mutableStateOf(interactiveSecondary) |
||||
private set |
||||
var interactiveMask by mutableStateOf(interactiveMask) |
||||
private set |
||||
var textPrimary by mutableStateOf(textPrimary) |
||||
private set |
||||
var textSecondary by mutableStateOf(textSecondary) |
||||
private set |
||||
var textHelp by mutableStateOf(textHelp) |
||||
private set |
||||
var textInteractive by mutableStateOf(textInteractive) |
||||
private set |
||||
var tornado1 by mutableStateOf(tornado1) |
||||
private set |
||||
var textLink by mutableStateOf(textLink) |
||||
private set |
||||
var iconPrimary by mutableStateOf(iconPrimary) |
||||
private set |
||||
var iconSecondary by mutableStateOf(iconSecondary) |
||||
private set |
||||
var iconInteractive by mutableStateOf(iconInteractive) |
||||
private set |
||||
var iconInteractiveInactive by mutableStateOf(iconInteractiveInactive) |
||||
private set |
||||
var error by mutableStateOf(error) |
||||
private set |
||||
var notificationBadge by mutableStateOf(notificationBadge) |
||||
private set |
||||
var isDark by mutableStateOf(isDark) |
||||
private set |
||||
|
||||
fun update(other: JetsnackColors) { |
||||
gradient6_1 = other.gradient6_1 |
||||
gradient6_2 = other.gradient6_2 |
||||
gradient3_1 = other.gradient3_1 |
||||
gradient3_2 = other.gradient3_2 |
||||
gradient2_1 = other.gradient2_1 |
||||
gradient2_2 = other.gradient2_2 |
||||
gradient2_3 = other.gradient2_3 |
||||
brand = other.brand |
||||
brandSecondary = other.brandSecondary |
||||
uiBackground = other.uiBackground |
||||
uiBorder = other.uiBorder |
||||
uiFloated = other.uiFloated |
||||
interactivePrimary = other.interactivePrimary |
||||
interactiveSecondary = other.interactiveSecondary |
||||
interactiveMask = other.interactiveMask |
||||
textPrimary = other.textPrimary |
||||
textSecondary = other.textSecondary |
||||
textHelp = other.textHelp |
||||
textInteractive = other.textInteractive |
||||
textLink = other.textLink |
||||
tornado1 = other.tornado1 |
||||
iconPrimary = other.iconPrimary |
||||
iconSecondary = other.iconSecondary |
||||
iconInteractive = other.iconInteractive |
||||
iconInteractiveInactive = other.iconInteractiveInactive |
||||
error = other.error |
||||
notificationBadge = other.notificationBadge |
||||
isDark = other.isDark |
||||
} |
||||
|
||||
fun copy(): JetsnackColors = JetsnackColors( |
||||
gradient6_1 = gradient6_1, |
||||
gradient6_2 = gradient6_2, |
||||
gradient3_1 = gradient3_1, |
||||
gradient3_2 = gradient3_2, |
||||
gradient2_1 = gradient2_1, |
||||
gradient2_2 = gradient2_2, |
||||
gradient2_3 = gradient2_3, |
||||
brand = brand, |
||||
brandSecondary = brandSecondary, |
||||
uiBackground = uiBackground, |
||||
uiBorder = uiBorder, |
||||
uiFloated = uiFloated, |
||||
interactivePrimary = interactivePrimary, |
||||
interactiveSecondary = interactiveSecondary, |
||||
interactiveMask = interactiveMask, |
||||
textPrimary = textPrimary, |
||||
textSecondary = textSecondary, |
||||
textHelp = textHelp, |
||||
textInteractive = textInteractive, |
||||
textLink = textLink, |
||||
tornado1 = tornado1, |
||||
iconPrimary = iconPrimary, |
||||
iconSecondary = iconSecondary, |
||||
iconInteractive = iconInteractive, |
||||
iconInteractiveInactive = iconInteractiveInactive, |
||||
error = error, |
||||
notificationBadge = notificationBadge, |
||||
isDark = isDark, |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
fun ProvideJetsnackColors( |
||||
colors: JetsnackColors, |
||||
content: @Composable () -> Unit |
||||
) { |
||||
val colorPalette = remember { |
||||
// Explicitly creating a new object here so we don't mutate the initial [colors] |
||||
// provided, and overwrite the values set in it. |
||||
colors.copy() |
||||
} |
||||
colorPalette.update(colors) |
||||
CompositionLocalProvider(LocalJetsnackColors provides colorPalette, content = content) |
||||
} |
||||
|
||||
private val LocalJetsnackColors = staticCompositionLocalOf<JetsnackColors> { |
||||
error("No JetsnackColorPalette provided") |
||||
} |
||||
|
||||
/** |
||||
* A Material [Colors] implementation which sets all colors to [debugColor] to discourage usage of |
||||
* [MaterialTheme.colors] in preference to [JetsnackTheme.colors]. |
||||
*/ |
||||
fun debugColors( |
||||
darkTheme: Boolean, |
||||
debugColor: Color = Color.Magenta |
||||
) = Colors( |
||||
primary = debugColor, |
||||
primaryVariant = debugColor, |
||||
secondary = debugColor, |
||||
secondaryVariant = debugColor, |
||||
background = debugColor, |
||||
surface = debugColor, |
||||
error = debugColor, |
||||
onPrimary = debugColor, |
||||
onSecondary = debugColor, |
||||
onBackground = debugColor, |
||||
onSurface = debugColor, |
||||
onError = debugColor, |
||||
isLight = !darkTheme |
||||
) |
@ -0,0 +1,118 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.theme |
||||
|
||||
import androidx.compose.material.Typography |
||||
import androidx.compose.ui.text.TextStyle |
||||
import androidx.compose.ui.text.font.FontFamily |
||||
import androidx.compose.ui.text.font.FontWeight |
||||
import androidx.compose.ui.unit.sp |
||||
|
||||
var Montserrat: FontFamily? = null // init in platform code |
||||
var Karla: FontFamily? = null // init in platform code |
||||
|
||||
val Typography by lazy { |
||||
Typography( |
||||
h1 = TextStyle( |
||||
fontFamily = Montserrat, |
||||
fontSize = 96.sp, |
||||
fontWeight = FontWeight.Light, |
||||
lineHeight = 117.sp, |
||||
letterSpacing = (-1.5).sp, |
||||
), |
||||
h2 = TextStyle( |
||||
fontFamily = Montserrat, |
||||
fontSize = 60.sp, |
||||
fontWeight = FontWeight.Light, |
||||
lineHeight = 73.sp, |
||||
letterSpacing = (-0.5).sp |
||||
), |
||||
h3 = TextStyle( |
||||
fontFamily = Montserrat, |
||||
fontSize = 48.sp, |
||||
fontWeight = FontWeight.Normal, |
||||
lineHeight = 59.sp |
||||
), |
||||
h4 = TextStyle( |
||||
fontFamily = Montserrat, |
||||
fontSize = 30.sp, |
||||
fontWeight = FontWeight.SemiBold, |
||||
lineHeight = 37.sp |
||||
), |
||||
h5 = TextStyle( |
||||
fontFamily = Montserrat, |
||||
fontSize = 24.sp, |
||||
fontWeight = FontWeight.SemiBold, |
||||
lineHeight = 29.sp |
||||
), |
||||
h6 = TextStyle( |
||||
fontFamily = Montserrat, |
||||
fontSize = 20.sp, |
||||
fontWeight = FontWeight.SemiBold, |
||||
lineHeight = 24.sp |
||||
), |
||||
subtitle1 = TextStyle( |
||||
fontFamily = Montserrat, |
||||
fontSize = 16.sp, |
||||
fontWeight = FontWeight.SemiBold, |
||||
lineHeight = 24.sp, |
||||
letterSpacing = 0.15.sp |
||||
), |
||||
subtitle2 = TextStyle( |
||||
fontFamily = Karla, |
||||
fontSize = 14.sp, |
||||
fontWeight = FontWeight.Bold, |
||||
lineHeight = 24.sp, |
||||
letterSpacing = 0.1.sp |
||||
), |
||||
body1 = TextStyle( |
||||
fontFamily = Karla, |
||||
fontSize = 16.sp, |
||||
fontWeight = FontWeight.Normal, |
||||
lineHeight = 28.sp, |
||||
letterSpacing = 0.15.sp |
||||
), |
||||
body2 = TextStyle( |
||||
fontFamily = Montserrat, |
||||
fontSize = 14.sp, |
||||
fontWeight = FontWeight.Medium, |
||||
lineHeight = 20.sp, |
||||
letterSpacing = 0.25.sp |
||||
), |
||||
button = TextStyle( |
||||
fontFamily = Montserrat, |
||||
fontSize = 14.sp, |
||||
fontWeight = FontWeight.SemiBold, |
||||
lineHeight = 16.sp, |
||||
letterSpacing = 1.25.sp |
||||
), |
||||
caption = TextStyle( |
||||
fontFamily = Karla, |
||||
fontSize = 12.sp, |
||||
fontWeight = FontWeight.Bold, |
||||
lineHeight = 16.sp, |
||||
letterSpacing = 0.4.sp |
||||
), |
||||
overline = TextStyle( |
||||
fontFamily = Montserrat, |
||||
fontSize = 12.sp, |
||||
fontWeight = FontWeight.SemiBold, |
||||
lineHeight = 16.sp, |
||||
letterSpacing = 1.sp |
||||
) |
||||
) |
||||
} |
@ -0,0 +1,19 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.utils |
||||
|
||||
expect fun formatPrice(price: Long): String |
@ -0,0 +1,40 @@
|
||||
/* |
||||
* Copyright 2021 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.utils |
||||
|
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.outlined.ArrowBack |
||||
import androidx.compose.material.icons.outlined.ArrowForward |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.graphics.vector.ImageVector |
||||
import androidx.compose.ui.platform.LocalLayoutDirection |
||||
import androidx.compose.ui.unit.LayoutDirection |
||||
|
||||
/** |
||||
* Returns the correct icon based on the current layout direction. |
||||
*/ |
||||
@Composable |
||||
fun mirroringIcon(ltrIcon: ImageVector, rtlIcon: ImageVector): ImageVector = |
||||
if (LocalLayoutDirection.current == LayoutDirection.Ltr) ltrIcon else rtlIcon |
||||
|
||||
/** |
||||
* Returns the correct back navigation icon based on the current layout direction. |
||||
*/ |
||||
@Composable |
||||
fun mirroringBackIcon() = mirroringIcon( |
||||
ltrIcon = Icons.Outlined.ArrowBack, rtlIcon = Icons.Outlined.ArrowForward |
||||
) |
@ -0,0 +1,21 @@
|
||||
/* |
||||
* Copyright 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.example.jetsnack.ui.utils |
||||
|
||||
/** |
||||
* Moved to https://google.github.io/accompanist/systemuicontroller/ |
||||
*/ |
@ -0,0 +1,7 @@
|
||||
package com.example.jetsnack.model |
||||
|
||||
import java.util.* |
||||
|
||||
actual fun createRandomUUID(): Long { |
||||
return UUID.randomUUID().mostSignificantBits |
||||
} |
@ -0,0 +1,53 @@
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
import androidx.compose.animation.* |
||||
import androidx.compose.animation.core.TweenSpec |
||||
import androidx.compose.foundation.Image |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.runtime.* |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.graphics.ImageBitmap |
||||
import androidx.compose.ui.graphics.toComposeImageBitmap |
||||
import androidx.compose.ui.layout.ContentScale |
||||
import kotlinx.coroutines.Dispatchers |
||||
import kotlinx.coroutines.withContext |
||||
import java.net.URL |
||||
import javax.imageio.ImageIO |
||||
|
||||
|
||||
private val imagesCache = mutableMapOf<String, ImageBitmap>() |
||||
|
||||
@OptIn(ExperimentalAnimationApi::class) |
||||
@Composable |
||||
actual fun SnackAsyncImage(imageUrl: String, contentDescription: String?, modifier: Modifier) { |
||||
var img: ImageBitmap? by remember(imageUrl) { mutableStateOf(null) } |
||||
|
||||
|
||||
AnimatedContent(img, transitionSpec = { |
||||
fadeIn(TweenSpec()) with fadeOut(TweenSpec()) |
||||
}) { |
||||
if (img != null) { |
||||
Image(img!!, contentDescription = contentDescription, modifier = modifier, contentScale = ContentScale.Crop) |
||||
} else { |
||||
Box(modifier = modifier) |
||||
} |
||||
} |
||||
|
||||
LaunchedEffect(imageUrl) { |
||||
if (imagesCache.contains(imageUrl)) { |
||||
img = imagesCache[imageUrl] |
||||
} else { |
||||
withContext(Dispatchers.IO) { |
||||
img = try { |
||||
ImageIO.read(URL(imageUrl)).toComposeImageBitmap().also { |
||||
imagesCache[imageUrl] = it |
||||
img = it |
||||
} |
||||
} catch (e: Throwable) { |
||||
e.printStackTrace() |
||||
null |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,10 @@
|
||||
package com.example.jetsnack.ui.utils |
||||
|
||||
import java.math.BigDecimal |
||||
import java.text.NumberFormat |
||||
|
||||
actual fun formatPrice(price: Long): String { |
||||
return NumberFormat.getCurrencyInstance().format( |
||||
BigDecimal(price).movePointLeft(2) |
||||
) |
||||
} |
@ -0,0 +1,13 @@
|
||||
package com.example.jetsnack.model |
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi |
||||
import platform.CoreFoundation.CFUUIDCreate |
||||
import platform.CoreFoundation.CFUUIDCreateString |
||||
|
||||
@OptIn(ExperimentalForeignApi::class) |
||||
actual fun createRandomUUID(): Long { |
||||
val uuidRef = CFUUIDCreate(null) |
||||
val uuidStringRef = CFUUIDCreateString(null, uuidRef) as String |
||||
val uuidStr: String = uuidStringRef.replace("-", "") |
||||
return uuidStr.substring(uuidStr.length - 16).toLong(16) |
||||
} |
@ -0,0 +1,86 @@
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
import androidx.compose.animation.* |
||||
import androidx.compose.animation.core.TweenSpec |
||||
import androidx.compose.foundation.Image |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.runtime.* |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.graphics.ImageBitmap |
||||
import androidx.compose.ui.graphics.toComposeImageBitmap |
||||
import androidx.compose.ui.layout.ContentScale |
||||
import kotlinx.cinterop.ExperimentalForeignApi |
||||
import kotlinx.cinterop.addressOf |
||||
import kotlinx.cinterop.usePinned |
||||
import kotlinx.coroutines.* |
||||
import org.jetbrains.skia.Image |
||||
import platform.Foundation.* |
||||
import platform.posix.memcpy |
||||
import kotlin.coroutines.resume |
||||
import kotlin.coroutines.resumeWithException |
||||
|
||||
private val imagesCache = mutableMapOf<String, ImageBitmap>() |
||||
|
||||
@OptIn(ExperimentalAnimationApi::class) |
||||
@Composable |
||||
actual fun SnackAsyncImage(imageUrl: String, contentDescription: String?, modifier: Modifier) { |
||||
var img: ImageBitmap? by remember(imageUrl) { mutableStateOf(null) } |
||||
|
||||
|
||||
AnimatedContent(img, transitionSpec = { |
||||
fadeIn(TweenSpec()) with fadeOut(TweenSpec()) |
||||
}) { |
||||
if (img != null) { |
||||
Image(img!!, contentDescription = contentDescription, modifier = modifier, contentScale = ContentScale.Crop) |
||||
} else { |
||||
Box(modifier = modifier) |
||||
} |
||||
} |
||||
|
||||
LaunchedEffect(imageUrl) { |
||||
if (imagesCache.contains(imageUrl)) { |
||||
img = imagesCache[imageUrl] |
||||
} else { |
||||
withContext(Dispatchers.IO) { |
||||
img = try { |
||||
loadImage(imageUrl).also { |
||||
imagesCache[imageUrl] = it |
||||
img = it |
||||
} |
||||
} catch (e: Throwable) { |
||||
e.printStackTrace() |
||||
null |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@OptIn(ExperimentalForeignApi::class) |
||||
suspend fun loadImage(url: String): ImageBitmap = suspendCancellableCoroutine { continuation -> |
||||
val nsUrl = NSURL(string = url) |
||||
val task = NSURLSession.sharedSession.dataTaskWithURL(nsUrl) { data, response, error -> |
||||
if (data != null) { |
||||
val byteArray = ByteArray(data.length.toInt()).apply { |
||||
usePinned { |
||||
memcpy( |
||||
it.addressOf(0), |
||||
data.bytes, |
||||
data.length |
||||
) |
||||
} |
||||
} |
||||
|
||||
continuation.resume(Image.makeFromEncoded(byteArray).toComposeImageBitmap()) |
||||
} else { |
||||
error?.let { |
||||
continuation.resumeWithException(Exception(it.localizedDescription)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
task.resume() |
||||
continuation.invokeOnCancellation { |
||||
task.cancel() |
||||
} |
||||
} |
@ -0,0 +1,10 @@
|
||||
package com.example.jetsnack.ui |
||||
|
||||
import androidx.compose.ui.window.ComposeUIViewController |
||||
import com.example.jetsnack.JetSnackAppEntryPoint |
||||
import platform.UIKit.UIViewController |
||||
|
||||
fun MainViewController(): UIViewController = |
||||
ComposeUIViewController { |
||||
JetSnackAppEntryPoint() |
||||
} |
@ -0,0 +1,14 @@
|
||||
package com.example.jetsnack.ui.utils |
||||
|
||||
import platform.Foundation.* |
||||
|
||||
actual fun formatPrice(price: Long): String { |
||||
val priceAsDouble = price / 100.0 |
||||
|
||||
val formatter = NSNumberFormatter() |
||||
formatter.setLocale(NSLocale.currentLocale) |
||||
formatter.numberStyle = NSNumberFormatterCurrencyStyle |
||||
|
||||
val numberPrice = NSNumber.numberWithDouble(priceAsDouble) |
||||
return formatter.stringFromNumber(numberPrice) ?: "" |
||||
} |
@ -0,0 +1,12 @@
|
||||
package com.example.jetsnack |
||||
|
||||
//import androidx.compose.desktop.ui.tooling.preview.Preview |
||||
import androidx.compose.runtime.Composable |
||||
import com.example.jetsnack.ui.JetsnackApp |
||||
|
||||
//@Preview |
||||
@Composable |
||||
fun AppPreview() { |
||||
JetsnackApp() |
||||
// App() |
||||
} |
@ -0,0 +1,15 @@
|
||||
package com.example.jetsnack |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.CompositionLocalProvider |
||||
import com.example.jetsnack.ui.JetsnackApp |
||||
|
||||
@Composable |
||||
fun JetSnackAppEntryPoint() { |
||||
CompositionLocalProvider( |
||||
strsLocal provides buildStingsResources(), |
||||
pluralsLocal provides buildPluralResources() |
||||
) { |
||||
JetsnackApp() |
||||
} |
||||
} |
@ -0,0 +1,23 @@
|
||||
package com.example.jetsnack |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.graphics.painter.Painter |
||||
import androidx.compose.ui.graphics.vector.VectorPainter |
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter |
||||
import com.example.jetsnack.MppR |
||||
import com.example.jetsnack.ui.myiconpack.EmptyStateSearch |
||||
import org.jetbrains.skiko.currentNanoTime |
||||
|
||||
@Composable |
||||
actual fun painterResource(id: Int): Painter { |
||||
return when(id) { |
||||
MppR.drawable.empty_state_search -> rememberVectorPainter(EmptyStateSearch) |
||||
else -> TODO() |
||||
} |
||||
} |
||||
|
||||
private var lastId = currentNanoTime().toInt() |
||||
|
||||
|
||||
private val _empty_state_search = lastId++ |
||||
actual val MppR.drawable.empty_state_search: Int get() = _empty_state_search |
@ -0,0 +1,84 @@
|
||||
package com.example.jetsnack |
||||
|
||||
fun buildStingsResources(): Map<Int, String> { |
||||
val strs = mutableMapOf<Int, String>() |
||||
val rs = MppR.string |
||||
|
||||
strs[rs.label_filters] = "Filters" |
||||
strs[rs.quantity] = "Qty" |
||||
strs[rs.label_decrease] = "Decrease" |
||||
strs[rs.label_increase] = "Increase" |
||||
|
||||
strs[rs.label_back] = "Back" |
||||
strs[rs.detail_header] = "Details" |
||||
strs[rs.detail_placeholder] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut tempus, sem vitae convallis imperdiet, lectus nunc pharetra diam, ac rhoncus quam eros eu risus. Nulla pulvinar condimentum erat, pulvinar tempus turpis blandit ut. Etiam sed ipsum sed lacus eleifend hendrerit eu quis quam. Etiam ligula eros, finibus vestibulum tortor ac, ultrices accumsan dolor. Vivamus vel nisl a libero lobortis posuere. Aenean facilisis nibh vel ultrices bibendum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse ac est vitae lacus commodo efficitur at ut massa. Etiam vestibulum sit amet sapien sed varius. Aliquam non ipsum imperdiet, pulvinar enim nec, mollis risus. Fusce id tincidunt nisl." |
||||
strs[rs.see_more] = "SEE MORE" |
||||
strs[rs.see_less] = "SEE LESS" |
||||
strs[rs.ingredients] = "Ingredients" |
||||
strs[rs.ingredients_list] = "Vanilla, Almond Flour, Eggs, Butter, Cream, Sugar" |
||||
strs[rs.add_to_cart] = "ADD TO CART" |
||||
strs[rs.label_select_delivery] = "Select delivery address" |
||||
|
||||
strs[rs.max_calories] = "Max Calories" |
||||
strs[rs.per_serving] = "per serving" |
||||
strs[rs.sort] = "Sort" |
||||
strs[rs.lifestyle] = "Lifestyle" |
||||
strs[rs.category] = "Category" |
||||
strs[rs.price] = "Price" |
||||
strs[rs.reset] = "Reset" |
||||
strs[rs.close] = "Close" |
||||
|
||||
|
||||
strs[rs.work_in_progress] = "This is currently work in progress" |
||||
strs[rs.grab_beverage] = "Grab a beverage and check back later!" |
||||
|
||||
strs[rs.home_feed] = "Home" |
||||
strs[rs.home_search] = "Search" |
||||
strs[rs.home_cart] = "My Cart" |
||||
strs[rs.home_profile] = "Profile" |
||||
|
||||
|
||||
strs[rs.search_no_matches] = "No matches for “%1s”" |
||||
strs[rs.search_no_matches_retry] = "Try broadening your search" |
||||
strs[rs.label_add] = "Add to cart" |
||||
strs[rs.search_count] = "%1d items" |
||||
strs[rs.label_search] = "Perform search" |
||||
strs[rs.search_jetsnack] = "Search Jetsnack" |
||||
strs[rs.cart_increase_error] = "There was an error and the quantity couldn\\'t be increased. Please try again." |
||||
strs[rs.cart_increase_error] = "There was an error and the quantity couldn\\'t be decreased. Please try again." |
||||
|
||||
// Cart |
||||
strs[rs.cart_order_header] = "Order (%1s)" |
||||
strs[rs.remove_item] = "Remove Item" |
||||
strs[rs.cart_summary_header] = "Summary" |
||||
strs[rs.cart_subtotal_label] = "Subtotal" |
||||
strs[rs.cart_shipping_label] = "Shipping & Handling" |
||||
strs[rs.cart_total_label] = "Total" |
||||
strs[rs.cart_checkout] = "Checkout" |
||||
strs[rs.label_remove] = "Remove item" |
||||
|
||||
return strs |
||||
} |
||||
|
||||
class PluralResource(val items: Map<String, String>) { |
||||
|
||||
// TODO: this is very dumb implementation, which works only for `one` or `other` |
||||
fun forQuantity(qty: Int): String { |
||||
return when (qty) { |
||||
1 -> items["one"] ?: "?????" |
||||
else -> items["other"] ?: "?????" |
||||
} |
||||
} |
||||
} |
||||
|
||||
fun buildPluralResources(): Map<Int, PluralResource> { |
||||
val plurals = mutableMapOf<Int, PluralResource>() |
||||
val ps = MppR.plurals |
||||
|
||||
plurals[ps.cart_order_count] = PluralResource(buildMap { |
||||
this["one"] = "%1d item" |
||||
this["other"] = "%1d items" |
||||
}) |
||||
|
||||
return plurals |
||||
} |
@ -0,0 +1,176 @@
|
||||
@file:Suppress("PrivatePropertyName") |
||||
|
||||
package com.example.jetsnack |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.CompositionLocal |
||||
import androidx.compose.runtime.compositionLocalOf |
||||
import org.jetbrains.skiko.currentNanoTime |
||||
|
||||
|
||||
val strsLocal = compositionLocalOf { emptyMap<Int, String>() } // intId to String |
||||
val pluralsLocal = compositionLocalOf { emptyMap<Int, PluralResource>() } |
||||
|
||||
@Composable |
||||
actual fun stringResource(id: Int): String { |
||||
return strsLocal.current[id] ?: "TODO" |
||||
} |
||||
|
||||
@Composable |
||||
actual fun stringResource(id: Int, part: String): String { |
||||
return strsLocal.current[id]?.replace("%1s", part) ?: "TODO" |
||||
} |
||||
|
||||
@Composable |
||||
actual fun stringResource(id: Int, count: Int): String { |
||||
return strsLocal.current[id]?.replace("%1d", count.toString()) ?: "TODO" |
||||
} |
||||
|
||||
private var lastId = currentNanoTime().toInt() |
||||
|
||||
// Filters |
||||
private var _label_filters = lastId++ |
||||
actual val MppR.string.label_filters: Int get() = _label_filters |
||||
|
||||
// Qty |
||||
private var _quantity = lastId++ |
||||
actual val MppR.string.quantity: Int get() = _quantity |
||||
|
||||
private val _label_decrease = lastId++ |
||||
actual val MppR.string.label_decrease: Int get() = _label_decrease |
||||
|
||||
private val _label_increase = lastId++ |
||||
actual val MppR.string.label_increase: Int get() = _label_increase |
||||
|
||||
|
||||
// Snack detail |
||||
private val _label_back = lastId++ |
||||
actual val MppR.string.label_back: Int get() = _label_back |
||||
|
||||
private val _detail_header = lastId++ |
||||
actual val MppR.string.detail_header: Int get() = _detail_header |
||||
|
||||
private val _detail_placeholder = lastId++ |
||||
actual val MppR.string.detail_placeholder: Int get() = _detail_placeholder |
||||
|
||||
private val _see_more = lastId++ |
||||
actual val MppR.string.see_more: Int get() = _see_more |
||||
|
||||
private val _see_less = lastId++ |
||||
actual val MppR.string.see_less: Int get() = _see_less |
||||
|
||||
private val _ingredients = lastId++ |
||||
actual val MppR.string.ingredients: Int get() = _ingredients |
||||
|
||||
private val _ingredients_list = lastId++ |
||||
actual val MppR.string.ingredients_list: Int get() = _ingredients_list |
||||
|
||||
private val _add_to_cart = lastId++ |
||||
actual val MppR.string.add_to_cart: Int get() = _add_to_cart |
||||
|
||||
// Home |
||||
private val _label_select_delivery = lastId++ |
||||
actual val MppR.string.label_select_delivery: Int get() = _label_select_delivery |
||||
|
||||
|
||||
// Filter |
||||
private val _max_calories = lastId++ |
||||
actual val MppR.string.max_calories: Int get() = _max_calories |
||||
|
||||
private val _per_serving = lastId++ |
||||
actual val MppR.string.per_serving: Int get() = _per_serving |
||||
|
||||
private val _sort = lastId++ |
||||
actual val MppR.string.sort: Int get() = _sort |
||||
|
||||
private val _lifestyle = lastId++ |
||||
actual val MppR.string.lifestyle: Int get() = _lifestyle |
||||
|
||||
private val _category = lastId++ |
||||
actual val MppR.string.category: Int get() = _category |
||||
|
||||
private val _price = lastId++ |
||||
actual val MppR.string.price: Int get() = _price |
||||
|
||||
private val _reset = lastId++ |
||||
actual val MppR.string.reset: Int get() = _reset |
||||
|
||||
private val _close = lastId++ |
||||
actual val MppR.string.close: Int get() = _close |
||||
|
||||
// Profile |
||||
|
||||
private val _work_in_progress = lastId++ |
||||
actual val MppR.string.work_in_progress: Int get() = _work_in_progress |
||||
|
||||
private val _grab_beverage = lastId++ |
||||
actual val MppR.string.grab_beverage: Int get() = _grab_beverage |
||||
|
||||
// Home |
||||
private val _home_feed = lastId++ |
||||
actual val MppR.string.home_feed: Int get() = _home_feed |
||||
|
||||
private val _home_search = lastId++ |
||||
actual val MppR.string.home_search: Int get() = _home_search |
||||
|
||||
private val _home_cart = lastId++ |
||||
actual val MppR.string.home_cart: Int get() = _home_cart |
||||
|
||||
private val _home_profile = lastId++ |
||||
actual val MppR.string.home_profile: Int get() = _home_profile |
||||
|
||||
|
||||
// Search |
||||
private val _search_no_matches = lastId++ |
||||
actual val MppR.string.search_no_matches: Int get() = _search_no_matches |
||||
|
||||
private val _search_no_matches_retry = lastId++ |
||||
actual val MppR.string.search_no_matches_retry: Int get() = _search_no_matches_retry |
||||
|
||||
private val _label_add = lastId++ |
||||
actual val MppR.string.label_add: Int get() = _label_add |
||||
|
||||
private val _search_count = lastId++ |
||||
actual val MppR.string.search_count: Int get() = _search_count |
||||
|
||||
private val _label_search = lastId++ |
||||
actual val MppR.string.label_search: Int get() = _label_search |
||||
|
||||
private val _search_jetsnack = lastId++ |
||||
actual val MppR.string.search_jetsnack: Int get() = _search_jetsnack |
||||
|
||||
private val _cart_increase_error = lastId++ |
||||
actual val MppR.string.cart_increase_error: Int get() = _cart_increase_error |
||||
|
||||
private val _cart_decrease_error = lastId++ |
||||
actual val MppR.string.cart_decrease_error: Int get() = _cart_decrease_error |
||||
|
||||
|
||||
// Cart |
||||
|
||||
private val _cart_order_count = lastId++ |
||||
actual val MppR.plurals.cart_order_count: Int get() = _cart_order_count |
||||
|
||||
private val _cart_order_header = lastId++ |
||||
actual val MppR.string.cart_order_header: Int get() = _cart_order_header |
||||
|
||||
private val _remove_item = lastId++ |
||||
actual val MppR.string.remove_item: Int get() = _remove_item |
||||
|
||||
private val _cart_summary_header = lastId++ |
||||
actual val MppR.string.cart_summary_header: Int get() = _cart_summary_header |
||||
|
||||
private val _cart_subtotal_label = lastId++ |
||||
actual val MppR.string.cart_subtotal_label: Int get() = _cart_subtotal_label |
||||
|
||||
private val _cart_shipping_label = lastId++ |
||||
actual val MppR.string.cart_shipping_label: Int get() = _cart_shipping_label |
||||
|
||||
private val _cart_total_label = lastId++ |
||||
actual val MppR.string.cart_total_label: Int get() = _cart_total_label |
||||
|
||||
private val _cart_checkout = lastId++ |
||||
actual val MppR.string.cart_checkout: Int get() = _cart_checkout |
||||
|
||||
private val _label_remove = lastId++ |
||||
actual val MppR.string.label_remove: Int get() = _label_remove |
@ -0,0 +1,86 @@
|
||||
package com.example.jetsnack.ui |
||||
|
||||
import androidx.compose.animation.* |
||||
import androidx.compose.animation.core.tween |
||||
import androidx.compose.foundation.layout.PaddingValues |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.mutableStateListOf |
||||
import androidx.compose.runtime.mutableStateOf |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.snapshots.Snapshot |
||||
import androidx.compose.ui.Modifier |
||||
import com.example.jetsnack.ui.home.CartTodo |
||||
import com.example.jetsnack.ui.home.Feed |
||||
import com.example.jetsnack.ui.home.HomeSections |
||||
import com.example.jetsnack.ui.home.Profile |
||||
import com.example.jetsnack.ui.home.cart.Cart |
||||
import com.example.jetsnack.ui.home.search.Search |
||||
import com.example.jetsnack.ui.snackdetail.SnackDetail |
||||
|
||||
@OptIn(ExperimentalAnimationApi::class) |
||||
@Composable |
||||
actual fun JetsnackScaffoldContent( |
||||
innerPaddingModifier: PaddingValues, |
||||
appState: MppJetsnackAppState |
||||
) { |
||||
|
||||
when (appState.currentRoute) { |
||||
HomeSections.FEED.route -> { |
||||
Feed( |
||||
onSnackClick = appState::navigateToSnackDetail, |
||||
modifier = Modifier.padding(innerPaddingModifier) |
||||
) |
||||
} |
||||
|
||||
HomeSections.SEARCH.route -> { |
||||
Search( |
||||
onSnackClick = appState::navigateToSnackDetail, |
||||
modifier = Modifier.padding(innerPaddingModifier) |
||||
) |
||||
} |
||||
|
||||
HomeSections.CART.route -> { |
||||
Cart( |
||||
onSnackClick = appState::navigateToSnackDetail, |
||||
modifier = Modifier.padding(innerPaddingModifier) |
||||
) |
||||
} |
||||
|
||||
HomeSections.PROFILE.route -> { |
||||
Profile(modifier = Modifier.padding(innerPaddingModifier)) |
||||
} |
||||
|
||||
else -> { |
||||
val snackId = appState.currentRoute?.takeIf { |
||||
it.startsWith(MainDestinations.SNACK_DETAIL_ROUTE + "/") |
||||
}?.let { |
||||
it.split("/")[1].toLongOrNull() |
||||
} |
||||
if (snackId != null) { |
||||
SnackDetail(snackId, appState::upPress, appState::navigateToSnackDetail) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
class NavigationStack<T>(initial: T) { |
||||
private val stack = mutableStateListOf(initial) |
||||
fun push(t: T) { |
||||
stack.add(t) |
||||
} |
||||
|
||||
fun replaceBy(t: T) { |
||||
stack.removeLast() |
||||
stack.add(t) |
||||
} |
||||
|
||||
fun back() { |
||||
if(stack.size > 1) { |
||||
// Always keep one element on the view stack |
||||
stack.removeLast() |
||||
} |
||||
} |
||||
|
||||
fun lastWithIndex() = stack.withIndex().last() |
||||
} |
@ -0,0 +1,52 @@
|
||||
package com.example.jetsnack.ui |
||||
|
||||
import androidx.compose.material.ScaffoldState |
||||
import androidx.compose.material.rememberScaffoldState |
||||
import androidx.compose.runtime.* |
||||
import com.example.jetsnack.model.SnackbarManager |
||||
import com.example.jetsnack.ui.home.HomeSections |
||||
import kotlinx.coroutines.CoroutineScope |
||||
|
||||
@Stable |
||||
actual class MppJetsnackAppState( |
||||
actual val scaffoldState: ScaffoldState, |
||||
actual val snackbarManager: SnackbarManager, |
||||
actual val coroutineScope: CoroutineScope, |
||||
) { |
||||
actual val bottomBarTabs: Array<HomeSections> |
||||
get() = HomeSections.values() |
||||
|
||||
private val navigationStack = NavigationStack(HomeSections.FEED.route) |
||||
|
||||
actual val currentRoute: String? |
||||
get() = navigationStack.lastWithIndex().value |
||||
|
||||
|
||||
@Composable |
||||
actual fun shouldShowBottomBar(): Boolean { |
||||
return currentRoute?.startsWith(MainDestinations.SNACK_DETAIL_ROUTE) != true |
||||
} |
||||
|
||||
actual fun navigateToBottomBarRoute(route: String) { |
||||
navigationStack.replaceBy(route) |
||||
} |
||||
|
||||
fun navigateToSnackDetail(snackId: Long) { |
||||
navigationStack.push("${MainDestinations.SNACK_DETAIL_ROUTE}/$snackId") |
||||
} |
||||
|
||||
fun upPress() { |
||||
navigationStack.back() |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
actual fun rememberMppJetsnackAppState(): MppJetsnackAppState { |
||||
val scaffoldState = rememberScaffoldState() |
||||
val snackbarManager = SnackbarManager |
||||
val coroutineScope = rememberCoroutineScope() |
||||
|
||||
return remember(scaffoldState, snackbarManager, coroutineScope) { |
||||
MppJetsnackAppState(scaffoldState, snackbarManager, coroutineScope) |
||||
} |
||||
} |
@ -0,0 +1,42 @@
|
||||
package com.example.jetsnack.ui.home |
||||
|
||||
import androidx.compose.foundation.Image |
||||
import androidx.compose.foundation.layout.* |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.text.style.TextAlign |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.* |
||||
|
||||
@Composable |
||||
fun CartTodo(modifier: Modifier = Modifier) { |
||||
Column( |
||||
horizontalAlignment = Alignment.CenterHorizontally, |
||||
modifier = modifier |
||||
.fillMaxSize() |
||||
.wrapContentSize() |
||||
.padding(24.dp) |
||||
) { |
||||
Image( |
||||
painterResource(MppR.drawable.empty_state_search), |
||||
contentDescription = null |
||||
) |
||||
Spacer(Modifier.height(24.dp)) |
||||
Text( |
||||
text = stringResource(MppR.string.work_in_progress), |
||||
style = MaterialTheme.typography.subtitle1, |
||||
textAlign = TextAlign.Center, |
||||
modifier = Modifier.fillMaxWidth() |
||||
) |
||||
Spacer(Modifier.height(16.dp)) |
||||
Text( |
||||
text = stringResource(MppR.string.grab_beverage), |
||||
style = MaterialTheme.typography.body2, |
||||
textAlign = TextAlign.Center, |
||||
modifier = Modifier.fillMaxWidth() |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,9 @@
|
||||
package com.example.jetsnack.ui.home |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.window.Popup |
||||
|
||||
@Composable |
||||
actual fun SnackDialog(onCloseRequest: () -> Unit, content: @Composable () -> Unit) { |
||||
Popup(onDismissRequest = onCloseRequest, content = { content() }) |
||||
} |
@ -0,0 +1,112 @@
|
||||
package com.example.jetsnack.ui.home.cart |
||||
|
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.layout.* |
||||
import androidx.compose.material.Icon |
||||
import androidx.compose.material.IconButton |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.filled.Close |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.unit.dp |
||||
import com.example.jetsnack.MppR |
||||
import com.example.jetsnack.label_remove |
||||
import com.example.jetsnack.model.OrderLine |
||||
import com.example.jetsnack.model.SnackRepo |
||||
import com.example.jetsnack.model.SnackbarManager |
||||
import com.example.jetsnack.pluralsLocal |
||||
import com.example.jetsnack.stringResource |
||||
import com.example.jetsnack.ui.components.QuantitySelector |
||||
import com.example.jetsnack.ui.components.SnackImage |
||||
import com.example.jetsnack.ui.theme.JetsnackTheme |
||||
import com.example.jetsnack.ui.utils.formatPrice |
||||
|
||||
@Composable |
||||
actual fun rememberQuantityString(res: Int, qty: Int, vararg args: Any): String { |
||||
val plurals = pluralsLocal.current |
||||
|
||||
return remember(res, qty, plurals) { |
||||
var str = plurals[res]?.forQuantity(qty) ?: "" |
||||
args.forEachIndexed { index, any -> |
||||
str = str.replace("%${index + 1}d", any.toString()) |
||||
} |
||||
str |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
actual fun ActualCartItem( |
||||
orderLine: OrderLine, |
||||
removeSnack: (Long) -> Unit, |
||||
increaseItemCount: (Long) -> Unit, |
||||
decreaseItemCount: (Long) -> Unit, |
||||
onSnackClick: (Long) -> Unit, |
||||
modifier: Modifier |
||||
) { |
||||
val snack = orderLine.snack |
||||
|
||||
Row(modifier = modifier |
||||
.fillMaxWidth() |
||||
.clickable { onSnackClick(snack.id) } |
||||
.background(JetsnackTheme.colors.uiBackground) |
||||
.padding(horizontal = 24.dp) |
||||
) { |
||||
SnackImage( |
||||
imageUrl = snack.imageUrl, |
||||
contentDescription = null, |
||||
modifier = Modifier.padding(top = 4.dp).size(100.dp) |
||||
) |
||||
Column(modifier = Modifier.padding(12.dp).weight(1f)) { |
||||
Text( |
||||
text = snack.name, |
||||
style = MaterialTheme.typography.subtitle1, |
||||
color = JetsnackTheme.colors.textSecondary, |
||||
) |
||||
Text( |
||||
text = snack.tagline, |
||||
style = MaterialTheme.typography.body1, |
||||
color = JetsnackTheme.colors.textHelp, |
||||
) |
||||
Text( |
||||
text = formatPrice(snack.price), |
||||
style = MaterialTheme.typography.subtitle1, |
||||
color = JetsnackTheme.colors.textPrimary, |
||||
modifier = Modifier.padding(top = 8.dp) |
||||
) |
||||
} |
||||
Column(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.End) { |
||||
IconButton( |
||||
onClick = { removeSnack(snack.id) }, |
||||
modifier = Modifier.padding(top = 12.dp) |
||||
) { |
||||
Icon( |
||||
imageVector = Icons.Filled.Close, |
||||
tint = JetsnackTheme.colors.iconSecondary, |
||||
contentDescription = stringResource(MppR.string.label_remove) |
||||
) |
||||
} |
||||
QuantitySelector( |
||||
count = orderLine.count, |
||||
decreaseItemCount = { decreaseItemCount(snack.id) }, |
||||
increaseItemCount = { increaseItemCount(snack.id) }, |
||||
modifier = Modifier.padding(top = 12.dp) |
||||
) |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
@Composable |
||||
actual fun getCartContentInsets(): WindowInsets { |
||||
return WindowInsets(top = 56.dp) |
||||
} |
||||
|
||||
@Composable |
||||
actual fun provideCartViewModel(): CartViewModel { |
||||
return remember { CartViewModel(SnackbarManager, SnackRepo) } |
||||
} |
@ -0,0 +1,15 @@
|
||||
package com.example.jetsnack.ui.home.cart |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.State |
||||
import androidx.compose.runtime.collectAsState |
||||
import com.example.jetsnack.model.OrderLine |
||||
import kotlinx.coroutines.flow.StateFlow |
||||
|
||||
actual abstract class JetSnackCartViewModel actual constructor() { |
||||
|
||||
@Composable |
||||
actual fun collectOrderLinesAsState(flow: StateFlow<List<OrderLine>>): State<List<OrderLine>> { |
||||
return flow.collectAsState() |
||||
} |
||||
} |
@ -0,0 +1,12 @@
|
||||
package com.example.jetsnack.ui.home |
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.unit.dp |
||||
|
||||
@Composable |
||||
actual fun snackCollectionListItemWindowInsets(): WindowInsets { |
||||
// TODO: implement |
||||
// WindowInsets.statusBars.add(WindowInsets(top = 56.dp)) |
||||
return WindowInsets(top = 56.dp) |
||||
} |
@ -0,0 +1,124 @@
|
||||
package com.example.jetsnack.ui.myiconpack |
||||
|
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd |
||||
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero |
||||
import androidx.compose.ui.graphics.SolidColor |
||||
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt |
||||
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter |
||||
import androidx.compose.ui.graphics.vector.ImageVector |
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder |
||||
import androidx.compose.ui.graphics.vector.path |
||||
import androidx.compose.ui.unit.dp |
||||
|
||||
public val EmptyStateSearch: ImageVector |
||||
get() { |
||||
if (_emptyStateSearch != null) { |
||||
return _emptyStateSearch!! |
||||
} |
||||
_emptyStateSearch = Builder(name = "EmptyStateSearch", defaultWidth = 341.0.dp, |
||||
defaultHeight = 179.0.dp, viewportWidth = 341.0f, viewportHeight = 179.0f).apply { |
||||
path(fill = SolidColor(Color(0xFFDDE3E8)), stroke = null, strokeLineWidth = 0.0f, |
||||
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, |
||||
pathFillType = NonZero) { |
||||
moveTo(302.676f, 111.056f) |
||||
lineTo(244.424f, 65.728f) |
||||
curveTo(234.123f, 57.654f, 224.238f, 49.061f, 214.807f, 39.98f) |
||||
curveTo(198.202f, 24.102f, 175.659f, 11.407f, 149.414f, 4.648f) |
||||
curveTo(85.649f, -11.772f, 35.135f, 17.344f, 12.16f, 60.096f) |
||||
curveTo(-22.949f, 125.426f, 20.921f, 195.341f, 105.817f, 175.009f) |
||||
curveTo(145.621f, 169.5f, 174.324f, 161.356f, 200.455f, 154.855f) |
||||
lineTo(295.072f, 135.285f) |
||||
lineTo(302.676f, 111.056f) |
||||
close() |
||||
} |
||||
path(fill = SolidColor(Color(0xFFffffff)), stroke = null, strokeLineWidth = 0.0f, |
||||
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, |
||||
pathFillType = NonZero) { |
||||
moveTo(288.225f, 120.035f) |
||||
arcToRelative(12.46f, 10.541f, 105.0f, true, false, 20.363f, 5.456f) |
||||
arcToRelative(12.46f, 10.541f, 105.0f, true, false, -20.363f, -5.456f) |
||||
close() |
||||
} |
||||
path(fill = SolidColor(Color(0xFF3C4043)), stroke = null, strokeLineWidth = 0.0f, |
||||
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, |
||||
pathFillType = NonZero) { |
||||
moveTo(299.659f, 110.277f) |
||||
curveTo(304.701f, 111.618f, 309.064f, 114.797f, 311.893f, 119.193f) |
||||
lineTo(313.356f, 121.465f) |
||||
lineTo(313.43f, 121.559f) |
||||
lineTo(339.097f, 129.093f) |
||||
curveTo(339.567f, 129.232f, 339.965f, 129.549f, 340.204f, 129.979f) |
||||
curveTo(340.444f, 130.408f, 340.505f, 130.914f, 340.376f, 131.389f) |
||||
lineTo(338.384f, 138.718f) |
||||
curveTo(338.319f, 138.957f, 338.208f, 139.18f, 338.056f, 139.376f) |
||||
curveTo(337.905f, 139.571f, 337.716f, 139.734f, 337.502f, 139.856f) |
||||
curveTo(337.287f, 139.979f, 337.051f, 140.057f, 336.806f, 140.087f) |
||||
curveTo(336.561f, 140.117f, 336.313f, 140.098f, 336.075f, 140.032f) |
||||
lineTo(310.402f, 132.833f) |
||||
lineTo(310.401f, 132.834f) |
||||
lineTo(307.823f, 133.812f) |
||||
curveTo(303.075f, 135.612f, 297.867f, 135.79f, 293.008f, 134.317f) |
||||
verticalLineTo(134.317f) |
||||
lineTo(299.659f, 110.277f) |
||||
close() |
||||
} |
||||
path(fill = SolidColor(Color(0xFF3C4043)), stroke = null, strokeLineWidth = 0.0f, |
||||
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, |
||||
pathFillType = EvenOdd) { |
||||
moveTo(161.472f, 52.165f) |
||||
lineTo(151.381f, 69.821f) |
||||
verticalLineTo(69.849f) |
||||
curveTo(160.101f, 74.643f, 167.496f, 81.558f, 172.896f, 89.966f) |
||||
curveTo(178.297f, 98.374f, 181.531f, 108.01f, 182.306f, 118.0f) |
||||
horizontalLineTo(61.0f) |
||||
curveTo(61.765f, 108.002f, 64.996f, 98.356f, 70.397f, 89.939f) |
||||
curveTo(75.798f, 81.523f, 83.198f, 74.602f, 91.925f, 69.807f) |
||||
lineTo(81.827f, 52.165f) |
||||
curveTo(81.551f, 51.678f, 81.478f, 51.101f, 81.624f, 50.56f) |
||||
curveTo(81.77f, 50.019f, 82.122f, 49.558f, 82.605f, 49.279f) |
||||
curveTo(83.087f, 49.001f, 83.659f, 48.927f, 84.195f, 49.074f) |
||||
curveTo(84.731f, 49.221f, 85.188f, 49.577f, 85.464f, 50.064f) |
||||
lineTo(95.687f, 67.93f) |
||||
curveTo(103.852f, 64.232f, 112.7f, 62.321f, 121.65f, 62.321f) |
||||
curveTo(130.599f, 62.321f, 139.448f, 64.232f, 147.613f, 67.93f) |
||||
lineTo(157.836f, 50.064f) |
||||
curveTo(158.112f, 49.577f, 158.568f, 49.221f, 159.104f, 49.074f) |
||||
curveTo(159.64f, 48.927f, 160.213f, 49.001f, 160.695f, 49.279f) |
||||
curveTo(161.177f, 49.558f, 161.53f, 50.019f, 161.676f, 50.56f) |
||||
curveTo(161.822f, 51.101f, 161.748f, 51.678f, 161.472f, 52.165f) |
||||
close() |
||||
moveTo(133.338f, 84.859f) |
||||
curveTo(133.338f, 79.463f, 128.696f, 75.709f, 121.95f, 75.709f) |
||||
curveTo(116.815f, 75.709f, 113.167f, 77.774f, 111.438f, 81.052f) |
||||
curveTo(110.345f, 83.124f, 111.889f, 85.617f, 114.226f, 85.617f) |
||||
curveTo(114.833f, 85.623f, 115.428f, 85.455f, 115.943f, 85.133f) |
||||
curveTo(116.457f, 84.81f, 116.869f, 84.346f, 117.129f, 83.797f) |
||||
curveTo(117.868f, 82.172f, 119.481f, 81.177f, 121.518f, 81.177f) |
||||
curveTo(124.199f, 81.177f, 126.358f, 82.82f, 126.358f, 85.058f) |
||||
curveTo(126.358f, 87.296f, 125.08f, 88.451f, 122.04f, 90.273f) |
||||
curveTo(118.783f, 92.186f, 117.488f, 94.496f, 117.794f, 98.214f) |
||||
lineTo(117.8f, 98.39f) |
||||
curveTo(117.813f, 98.76f, 117.968f, 99.109f, 118.233f, 99.366f) |
||||
curveTo(118.498f, 99.623f, 118.852f, 99.766f, 119.22f, 99.766f) |
||||
horizontalLineTo(122.669f) |
||||
curveTo(122.856f, 99.766f, 123.041f, 99.729f, 123.213f, 99.658f) |
||||
curveTo(123.386f, 99.586f, 123.543f, 99.481f, 123.675f, 99.349f) |
||||
curveTo(123.806f, 99.216f, 123.911f, 99.059f, 123.983f, 98.886f) |
||||
curveTo(124.054f, 98.713f, 124.091f, 98.528f, 124.091f, 98.341f) |
||||
curveTo(124.091f, 96.031f, 125.152f, 94.695f, 128.283f, 92.872f) |
||||
curveTo(131.611f, 90.905f, 133.338f, 88.433f, 133.338f, 84.859f) |
||||
close() |
||||
moveTo(121.068f, 102.925f) |
||||
curveTo(118.945f, 102.925f, 117.218f, 104.567f, 117.218f, 106.642f) |
||||
curveTo(117.218f, 108.736f, 118.927f, 110.36f, 121.068f, 110.36f) |
||||
curveTo(123.209f, 110.36f, 124.936f, 108.736f, 124.936f, 106.642f) |
||||
curveTo(124.936f, 104.549f, 123.209f, 102.925f, 121.068f, 102.925f) |
||||
close() |
||||
} |
||||
} |
||||
.build() |
||||
return _emptyStateSearch!! |
||||
} |
||||
|
||||
private var _emptyStateSearch: ImageVector? = null |
@ -0,0 +1,11 @@
|
||||
package com.example.jetsnack.ui.snackdetail |
||||
|
||||
import androidx.compose.ui.Modifier |
||||
|
||||
actual fun Modifier.jetSnackNavigationBarsPadding(): Modifier = this |
||||
// .navigationBarsPadding() |
||||
|
||||
actual fun Modifier.jetSnackStatusBarsPadding(): Modifier = this |
||||
// statusBarsPadding() |
||||
actual fun Modifier.jetSnackSystemBarsPadding(): Modifier = this |
||||
// Modifier.systemBarsPadding() |
@ -0,0 +1,8 @@
|
||||
package com.example.jetsnack.model |
||||
|
||||
import org.jetbrains.skiko.currentNanoTime |
||||
|
||||
actual fun createRandomUUID(): Long { |
||||
// TODO: implement. Create random UUID |
||||
return currentNanoTime() |
||||
} |
@ -0,0 +1,66 @@
|
||||
package com.example.jetsnack.ui.components |
||||
|
||||
import org.jetbrains.skia.ExternalSymbolName |
||||
import org.jetbrains.skia.impl.NativePointer |
||||
import org.khronos.webgl.ArrayBuffer |
||||
import org.khronos.webgl.Int8Array |
||||
import org.w3c.xhr.XMLHttpRequest |
||||
import kotlin.coroutines.resume |
||||
import kotlin.coroutines.resumeWithException |
||||
import kotlin.coroutines.suspendCoroutine |
||||
import kotlin.wasm.unsafe.UnsafeWasmMemoryApi |
||||
import kotlin.wasm.unsafe.withScopedMemoryAllocator |
||||
|
||||
private class MissingResourceException(url: String): Exception("GET $url failed") |
||||
|
||||
suspend fun loadImage(url: String): ArrayBuffer { |
||||
return suspendCoroutine { continuation -> |
||||
val req = XMLHttpRequest() |
||||
req.open("GET", url, true) |
||||
req.responseType = "arraybuffer".toJsString().unsafeCast() |
||||
|
||||
req.onload = { _ -> |
||||
val arrayBuffer = req.response |
||||
if (arrayBuffer is ArrayBuffer) { |
||||
continuation.resume(arrayBuffer) |
||||
} else { |
||||
continuation.resumeWithException(MissingResourceException(url)) |
||||
} |
||||
} |
||||
req.send("") |
||||
} |
||||
} |
||||
|
||||
fun ArrayBuffer.toByteArray(): ByteArray { |
||||
val source = Int8Array(this, 0, byteLength) |
||||
return jsInt8ArrayToKotlinByteArray(source) |
||||
} |
||||
|
||||
@JsFun( |
||||
""" (src, size, dstAddr) => { |
||||
const mem8 = new Int8Array(wasmExports.memory.buffer, dstAddr, size); |
||||
mem8.set(src); |
||||
} |
||||
""" |
||||
) |
||||
internal external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr: Int) |
||||
|
||||
internal fun jsInt8ArrayToKotlinByteArray(x: Int8Array): ByteArray { |
||||
val size = x.length |
||||
|
||||
@OptIn(UnsafeWasmMemoryApi::class) |
||||
return withScopedMemoryAllocator { allocator -> |
||||
val memBuffer = allocator.allocate(size) |
||||
val dstAddress = memBuffer.address.toInt() |
||||
jsExportInt8ArrayToWasm(x, size, dstAddress) |
||||
ByteArray(size) { i -> (memBuffer + i).loadByte() } |
||||
} |
||||
} |
||||
|
||||
@ExternalSymbolName("_malloc") |
||||
@kotlin.wasm.WasmImport("skia", "malloc") |
||||
private external fun _malloc(size: Int): NativePointer |
||||
|
||||
@ExternalSymbolName("_free") |
||||
@kotlin.wasm.WasmImport("skia", "free") |
||||
private external fun _free(ptr: NativePointer) |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue