Browse Source

Add jetsnack app

pull/4746/head
Artem Kobzar 2 weeks ago
parent
commit
707a6ccdf6
  1. 43
      examples/jetsnack/.gitignore
  2. 122
      examples/jetsnack/README.md
  3. 41
      examples/jetsnack/android/build.gradle.kts
  4. 17
      examples/jetsnack/android/src/main/AndroidManifest.xml
  5. 36
      examples/jetsnack/android/src/main/java/com/example/android/MainActivity.kt
  6. 29
      examples/jetsnack/build.gradle.kts
  7. 110
      examples/jetsnack/common/build.gradle.kts
  8. 2
      examples/jetsnack/common/src/androidMain/AndroidManifest.xml
  9. 13
      examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/drawableResources.android.kt
  10. 7
      examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/model/createRandomUUID.kt
  11. 114
      examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/stringResource.android.kt
  12. 74
      examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/JetsnackScaffoldContent.kt
  13. 123
      examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/MppJetsnackAppState.kt
  14. 25
      examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt
  15. 9
      examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/SnackDialog.kt
  16. 189
      examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.android.kt
  17. 48
      examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.android.kt
  18. 12
      examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/snackCollectionListItemWindowInsets.kt
  19. 10
      examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/snackdetail/jetSnackNavigationBarsPadding.kt
  20. 10
      examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.kt
  21. 33
      examples/jetsnack/common/src/androidMain/res/drawable-night/empty_state_search.xml
  22. BIN
      examples/jetsnack/common/src/androidMain/res/drawable-nodpi/placeholder.jpg
  23. 39
      examples/jetsnack/common/src/androidMain/res/drawable-v26/ic_launcher_foreground.xml
  24. 33
      examples/jetsnack/common/src/androidMain/res/drawable/empty_state_search.xml
  25. BIN
      examples/jetsnack/common/src/androidMain/res/font/karla_bold.ttf
  26. BIN
      examples/jetsnack/common/src/androidMain/res/font/karla_regular.ttf
  27. BIN
      examples/jetsnack/common/src/androidMain/res/font/montserrat_light.ttf
  28. BIN
      examples/jetsnack/common/src/androidMain/res/font/montserrat_medium.ttf
  29. BIN
      examples/jetsnack/common/src/androidMain/res/font/montserrat_regular.ttf
  30. BIN
      examples/jetsnack/common/src/androidMain/res/font/montserrat_semibold.ttf
  31. 19
      examples/jetsnack/common/src/androidMain/res/values-night/themes.xml
  32. 17
      examples/jetsnack/common/src/androidMain/res/values/colors.xml
  33. 80
      examples/jetsnack/common/src/androidMain/res/values/strings.xml
  34. 31
      examples/jetsnack/common/src/androidMain/res/values/themes.xml
  35. 220
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/AndroidxComposeMaterialIcons.kt
  36. 9
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/DrawableRes.kt
  37. 87
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/StringRes.kt
  38. 337
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/flowlayout/FlowAccompanist.kt
  39. 68
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Filter.kt
  40. 137
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Search.kt
  41. 226
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Snack.kt
  42. 113
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/SnackCollection.kt
  43. 50
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/SnackbarManager.kt
  44. 59
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/JetsnackApp.kt
  45. 166
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/JetsnackAppState.kt
  46. 126
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Button.kt
  47. 61
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Card.kt
  48. 56
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Divider.kt
  49. 162
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Filters.kt
  50. 81
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Gradient.kt
  51. 109
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/GradientTintedIconButton.kt
  52. 68
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Grid.kt
  53. 109
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/QuantitySelector.kt
  54. 80
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Scaffold.kt
  55. 55
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Snackbar.kt
  56. 308
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Snacks.kt
  57. 97
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Surface.kt
  58. 82
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/DestinationBar.kt
  59. 137
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Feed.kt
  60. 275
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/FilterScreen.kt
  61. 363
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Home.kt
  62. 74
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Profile.kt
  63. 343
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.kt
  64. 92
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.kt
  65. 68
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/SwipeDismissItem.kt
  66. 166
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Categories.kt
  67. 219
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Results.kt
  68. 259
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Search.kt
  69. 108
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Suggestions.kt
  70. 406
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/snackdetail/SnackDetail.kt
  71. 89
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Color.kt
  72. 27
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Shape.kt
  73. 310
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Theme.kt
  74. 118
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Type.kt
  75. 19
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/Currency.kt
  76. 40
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/Rtl.kt
  77. 21
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/SystemUi.kt
  78. 7
      examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/model/createRandomUUID.kt
  79. 53
      examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt
  80. 10
      examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.kt
  81. 13
      examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/model/createRandomUUID.ios.kt
  82. 86
      examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.ios.kt
  83. 10
      examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/main.ios.kt
  84. 14
      examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.ios.kt
  85. 12
      examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/DesktopApp.kt
  86. 15
      examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/JetsnakAppEntryPoint.kt
  87. 23
      examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/drawableResources.desktop.kt
  88. 84
      examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/initStringResource.kt
  89. 176
      examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/stringResource.kt
  90. 86
      examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/JetsnackScaffoldContent.kt
  91. 52
      examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/MppJetsnackAppState.kt
  92. 42
      examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/CartTodo.kt
  93. 9
      examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/SnackDialog.kt
  94. 112
      examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.nonAndroid.kt
  95. 15
      examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.nonAndroid.kt
  96. 12
      examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/snackCollectionListItemWindowInsets.kt
  97. 124
      examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/myiconpack/EmptyStateSearch.kt
  98. 11
      examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/snackdetail/jetSnackNavigationBarsPadding.kt
  99. 8
      examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/model/createRandomUUID.kt
  100. 66
      examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/ui/components/ImageLoader.kt
  101. Some files were not shown because too many files have changed in this diff Show More

43
examples/jetsnack/.gitignore vendored

@ -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

122
examples/jetsnack/README.md

@ -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>&nbsp;<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>&nbsp;<br>
* **Desktop version:**
`./gradlew :desktop:run`
<br>&nbsp;<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)

41
examples/jetsnack/android/build.gradle.kts

@ -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
}
}
}

17
examples/jetsnack/android/src/main/AndroidManifest.xml

@ -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>

36
examples/jetsnack/android/src/main/java/com/example/android/MainActivity.kt

@ -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()
}
}
}
}

29
examples/jetsnack/build.gradle.kts

@ -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
}

110
examples/jetsnack/common/build.gradle.kts

@ -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)
}
}

2
examples/jetsnack/common/src/androidMain/AndroidManifest.xml

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"/>

13
examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/drawableResources.android.kt

@ -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

7
examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/model/createRandomUUID.kt

@ -0,0 +1,7 @@
package com.example.jetsnack.model
import java.util.*
actual fun createRandomUUID(): Long {
return UUID.randomUUID().mostSignificantBits
}

114
examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/stringResource.android.kt

@ -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

74
examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/JetsnackScaffoldContent.kt

@ -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)
}
}

123
examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/MppJetsnackAppState.kt

@ -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
}

25
examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt

@ -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,
)
}

9
examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/SnackDialog.kt

@ -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)
}

189
examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.android.kt

@ -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))
}

48
examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.android.kt

@ -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()
}
}

12
examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/snackCollectionListItemWindowInsets.kt

@ -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))
}

10
examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/snackdetail/jetSnackNavigationBarsPadding.kt

@ -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()

10
examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.kt

@ -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)
)
}

33
examples/jetsnack/common/src/androidMain/res/drawable-night/empty_state_search.xml

@ -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>

BIN
examples/jetsnack/common/src/androidMain/res/drawable-nodpi/placeholder.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

39
examples/jetsnack/common/src/androidMain/res/drawable-v26/ic_launcher_foreground.xml

@ -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>

33
examples/jetsnack/common/src/androidMain/res/drawable/empty_state_search.xml

@ -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>

BIN
examples/jetsnack/common/src/androidMain/res/font/karla_bold.ttf

Binary file not shown.

BIN
examples/jetsnack/common/src/androidMain/res/font/karla_regular.ttf

Binary file not shown.

BIN
examples/jetsnack/common/src/androidMain/res/font/montserrat_light.ttf

Binary file not shown.

BIN
examples/jetsnack/common/src/androidMain/res/font/montserrat_medium.ttf

Binary file not shown.

BIN
examples/jetsnack/common/src/androidMain/res/font/montserrat_regular.ttf

Binary file not shown.

BIN
examples/jetsnack/common/src/androidMain/res/font/montserrat_semibold.ttf

Binary file not shown.

19
examples/jetsnack/common/src/androidMain/res/values-night/themes.xml

@ -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>

17
examples/jetsnack/common/src/androidMain/res/values/colors.xml

@ -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>

80
examples/jetsnack/common/src/androidMain/res/values/strings.xml

@ -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 &amp; 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>

31
examples/jetsnack/common/src/androidMain/res/values/themes.xml

@ -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>

220
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/AndroidxComposeMaterialIcons.kt

@ -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

9
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/DrawableRes.kt

@ -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

87
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/StringRes.kt

@ -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

337
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/flowlayout/FlowAccompanist.kt

@ -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);
}

68
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Filter.kt

@ -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

137
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Search.kt

@ -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"
)
)
)

226
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Snack.kt

@ -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
)
)

113
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/SnackCollection.kt

@ -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
)

50
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/SnackbarManager.kt

@ -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

59
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/JetsnackApp.kt

@ -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)

166
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/JetsnackAppState.kt

@ -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
//}

126
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Button.kt

@ -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")
}
}
}

61
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Card.kt

@ -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))
}
}
}

56
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Divider.kt

@ -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))
}
}
}

162
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Filters.kt

@ -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))
}
}

81
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Gradient.kt

@ -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
)
}

109
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/GradientTintedIconButton.kt

@ -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)
)
}
}

68
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Grid.kt

@ -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
}
}
}
}

109
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/QuantitySelector.kt

@ -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, {}, {})
}
}
}
}

80
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Scaffold.kt

@ -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
)
}

55
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Snackbar.kt

@ -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
)
}

308
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Snacks.kt

@ -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
)
}
}

97
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Surface.kt

@ -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)
}

82
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/DestinationBar.kt

@ -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()
}
}

137
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Feed.kt

@ -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 = { })
}
}

275
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/FilterScreen.kt

@ -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 = {})
}

363
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Home.kt

@ -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 = { }
)
}
}

74
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Profile.kt

@ -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()
}
}

343
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.kt

@ -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
)
}
}
}
}

92
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.kt

@ -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>>
}

68
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/SwipeDismissItem.kt

@ -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) }
)
}
}

166
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Categories.kt

@ -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
)
}
}

219
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Results.kt

@ -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
)
}
}
}

259
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Search.kt

@ -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
)
}
}
}

108
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Suggestions.kt

@ -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 = { }
)
}
}
}

406
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/snackdetail/SnackDetail.kt

@ -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 = { }
)
}
}

89
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Color.kt

@ -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

27
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Shape.kt

@ -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)
)

310
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Theme.kt

@ -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
)

118
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Type.kt

@ -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
)
)
}

19
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/Currency.kt

@ -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

40
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/Rtl.kt

@ -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
)

21
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/SystemUi.kt

@ -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/
*/

7
examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/model/createRandomUUID.kt

@ -0,0 +1,7 @@
package com.example.jetsnack.model
import java.util.*
actual fun createRandomUUID(): Long {
return UUID.randomUUID().mostSignificantBits
}

53
examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt

@ -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
}
}
}
}
}

10
examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.kt

@ -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)
)
}

13
examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/model/createRandomUUID.ios.kt

@ -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)
}

86
examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.ios.kt

@ -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()
}
}

10
examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/main.ios.kt

@ -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()
}

14
examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.ios.kt

@ -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) ?: ""
}

12
examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/DesktopApp.kt

@ -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()
}

15
examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/JetsnakAppEntryPoint.kt

@ -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()
}
}

23
examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/drawableResources.desktop.kt

@ -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

84
examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/initStringResource.kt

@ -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
}

176
examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/stringResource.kt

@ -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

86
examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/JetsnackScaffoldContent.kt

@ -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()
}

52
examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/MppJetsnackAppState.kt

@ -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)
}
}

42
examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/CartTodo.kt

@ -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()
)
}
}

9
examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/SnackDialog.kt

@ -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() })
}

112
examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.nonAndroid.kt

@ -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) }
}

15
examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.nonAndroid.kt

@ -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()
}
}

12
examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/snackCollectionListItemWindowInsets.kt

@ -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)
}

124
examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/myiconpack/EmptyStateSearch.kt

@ -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

11
examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/snackdetail/jetSnackNavigationBarsPadding.kt

@ -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()

8
examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/model/createRandomUUID.kt

@ -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()
}

66
examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/ui/components/ImageLoader.kt

@ -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…
Cancel
Save