Browse Source

Update jetsnack to use new libraries

ok/upd_jetsnack_to_new_libs
Oleksandr.Karpovich 2 months ago
parent
commit
e96ab10a4c
  1. 2
      examples/jetsnack/android/src/main/java/com/example/android/MainActivity.kt
  2. 18
      examples/jetsnack/common/build.gradle.kts
  3. 4
      examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/MppJetsnackAppState.kt
  4. 12
      examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.android.kt
  5. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/almonds.jpg
  6. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/apple_chips.jpg
  7. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/apple_juice.jpg
  8. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/apple_pie.jpg
  9. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/apple_sauce.jpg
  10. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/apples.jpg
  11. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/cheese.jpg
  12. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/chips.jpg
  13. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/cupcake.jpg
  14. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/desserts.jpg
  15. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/donut.jpg
  16. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/eclair.jpg
  17. 1
      examples/jetsnack/common/src/commonMain/composeResources/drawable/empty_state_search.svg
  18. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/froyo.jpg
  19. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/fruit.jpg
  20. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/gingerbread.jpg
  21. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/gluten_free.jpg
  22. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/grapes.jpg
  23. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/honeycomb.jpg
  24. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/ice_cream_sandwich.jpg
  25. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/jelly_bean.jpg
  26. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/kitkat.jpg
  27. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/kiwi.jpg
  28. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/lollipop.jpg
  29. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/mango.jpg
  30. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/marshmallow.jpg
  31. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/nougat.jpg
  32. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/nuts.jpg
  33. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/oreo.jpg
  34. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/organic.jpg
  35. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/paleo.jpg
  36. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/pie.jpg
  37. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/placeholder.jpg
  38. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/popcorn.jpg
  39. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/pretzels.jpg
  40. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/smoothies.jpg
  41. BIN
      examples/jetsnack/common/src/commonMain/composeResources/drawable/vegan.jpg
  42. 65
      examples/jetsnack/common/src/commonMain/composeResources/values/strings.xml
  43. 220
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/AndroidxComposeMaterialIcons.kt
  44. 9
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/DrawableRes.kt
  45. 87
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/StringRes.kt
  46. 16
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Filter.kt
  47. 30
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Search.kt
  48. 63
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Snack.kt
  49. 17
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/SnackCollection.kt
  50. 7
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/SnackbarManager.kt
  51. 151
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/JetsnackApp.kt
  52. 166
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/JetsnackAppState.kt
  53. 22
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/SnackSharedElementKey.kt
  54. 43
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Button.kt
  55. 17
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Card.kt
  56. 24
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Divider.kt
  57. 112
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Filters.kt
  58. 28
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Gradient.kt
  59. 18
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/GradientTintedIconButton.kt
  60. 67
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/QuantitySelector.kt
  61. 88
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Scaffold.kt
  62. 20
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Snackbar.kt
  63. 412
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Snacks.kt
  64. 11
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Surface.kt
  65. 105
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/DestinationBar.kt
  66. 116
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Feed.kt
  67. 215
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/FilterScreen.kt
  68. 179
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Home.kt
  69. 43
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Profile.kt
  70. 372
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.kt
  71. 38
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.kt
  72. 38
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/SwipeDismissItem.kt
  73. 24
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Categories.kt
  74. 225
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Results.kt
  75. 82
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Search.kt
  76. 31
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Suggestions.kt
  77. 107
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/navigation/JetsnackNavController.kt
  78. 681
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/snackdetail/SnackDetail.kt
  79. 2
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Shape.kt
  80. 42
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Theme.kt
  81. 176
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Type.kt
  82. 40
      examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/Rtl.kt
  83. 55
      examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt
  84. 59
      examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.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. 55
      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. 18
      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. 21
      examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/snackdetail/jetSnackNavigationBarsPadding.kt
  99. 19
      examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/ui/components/ImageLoader.kt
  100. 54
      examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt
  101. Some files were not shown because too many files have changed in this diff Show More

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

@ -3,7 +3,7 @@ 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.material3.MaterialTheme
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight

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

@ -38,14 +38,17 @@ kotlin {
dependencies {
api(compose.runtime)
api(compose.foundation)
api(compose.material)
api(compose.ui)
api(compose.material3)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.lifecycle.viewModelCompose)
implementation(libs.androidx.lifecycle.viewModel)
implementation(libs.compose.material.icons.core)
implementation(compose.materialIconsExtended)
implementation(compose.components.resources)
implementation(libs.kotlinx.coroutines)
}
}
val nonAndroidMain by creating {
dependsOn(commonMain)
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
@ -73,22 +76,23 @@ kotlin {
}
}
val desktopMain by getting {
dependsOn(nonAndroidMain)
dependsOn(commonMain)
dependencies {
api(compose.preview)
implementation(libs.kotlinx.coroutines.swing)
}
}
val desktopTest by getting
val wasmJsMain by getting {
dependsOn(commonMain)
dependencies {
implementation(kotlin("stdlib"))
}
dependsOn(nonAndroidMain)
}
val iosMain by getting {
dependsOn(nonAndroidMain)
dependsOn(commonMain)
}
}
}

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

@ -1,8 +1,8 @@
package com.example.jetsnack.ui
import android.content.res.Resources
import androidx.compose.material.ScaffoldState
import androidx.compose.material.rememberScaffoldState
import androidx.compose.material3.ScaffoldState
import androidx.compose.material3.rememberScaffoldState
import androidx.compose.runtime.*
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalConfiguration

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

@ -19,12 +19,12 @@ 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.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.icons.Icons
import androidx.compose.material3.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/almonds.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/apple_chips.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/apple_juice.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/apple_pie.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/apple_sauce.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/apples.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/cheese.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/chips.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/cupcake.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/desserts.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/donut.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/eclair.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

1
examples/jetsnack/common/src/commonMain/composeResources/drawable/empty_state_search.svg

@ -0,0 +1 @@
<svg id="vector" xmlns="http://www.w3.org/2000/svg" width="341" height="179" viewBox="0 0 341 179"><path fill="#DDE3E8" d="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" id="path_0"/><path fill="#ffffff" d="M288.225,120.035a12.46,10.541 105,1 0,20.363 5.456a12.46,10.541 105,1 0,-20.363 -5.456z" id="path_1"/><path fill="#3C4043" d="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" id="path_2"/><path fill="#3C4043" fill-rule="evenodd" d="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" id="path_3"/></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/froyo.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/fruit.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/gingerbread.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/gluten_free.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/grapes.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/honeycomb.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/ice_cream_sandwich.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/jelly_bean.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/kitkat.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/kiwi.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/lollipop.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/mango.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/marshmallow.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/nougat.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/nuts.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/oreo.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/organic.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/paleo.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/pie.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/placeholder.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/popcorn.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/pretzels.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/smoothies.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

BIN
examples/jetsnack/common/src/commonMain/composeResources/drawable/vegan.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

65
examples/jetsnack/common/src/commonMain/composeResources/values/strings.xml

@ -0,0 +1,65 @@
<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 “%1$s”</string>
<string name="search_no_matches_retry">Try broadening your search</string>
<string name="search_count">%1$d 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 (%1$s)</string>
<plurals name="cart_order_count">
<item quantity="one">%1$d item</item>
<item quantity="other">%1$d 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>

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

@ -1,220 +0,0 @@
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

@ -1,9 +0,0 @@
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

@ -1,87 +0,0 @@
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

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

@ -16,13 +16,13 @@
package com.example.jetsnack.model
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
//import androidx.compose.material3.icons.Icons
//import androidx.compose.material3.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
//import com.example.jetsnack.Android
//import com.example.jetsnack.SortByAlpha
@Stable
class Filter(
@ -45,10 +45,10 @@ val priceFilters = listOf(
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 sortFilters = listOf<Filter>(
Filter(name = "Android's favorite (default)"),
Filter(name = "Rating",),
Filter(name = "Alphabetical",)
)
val categoryFilters = listOf(

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

@ -17,9 +17,15 @@
package com.example.jetsnack.model
import androidx.compose.runtime.Immutable
import com.example.common.generated.resources.*
import com.example.common.generated.resources.Res
import com.example.common.generated.resources.chips
import com.example.common.generated.resources.desserts
import com.example.common.generated.resources.fruit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.DrawableResource
/**
* A fake repo for searching.
@ -44,7 +50,7 @@ data class SearchCategoryCollection(
@Immutable
data class SearchCategory(
val name: String,
val imageUrl: String
val image: DrawableResource
)
@Immutable
@ -58,26 +64,26 @@ data class SearchSuggestionGroup(
* Static data
*/
private val searchCategoryCollections = listOf(
private val searchCategoryCollections = listOf<SearchCategoryCollection>(
SearchCategoryCollection(
id = 0L,
name = "Categories",
categories = listOf(
SearchCategory(
name = "Chips & crackers",
imageUrl = "files/chips.jpg"
image = Res.drawable.chips
),
SearchCategory(
name = "Fruit snacks",
imageUrl = "files/fruit.jpg"
image = Res.drawable.fruit
),
SearchCategory(
name = "Desserts",
imageUrl = "files/desserts.jpg"
image = Res.drawable.desserts
),
SearchCategory(
name = "Nuts ",
imageUrl = "files/nuts.jpg"
image = Res.drawable.nuts
)
)
),
@ -87,27 +93,27 @@ private val searchCategoryCollections = listOf(
categories = listOf(
SearchCategory(
name = "Organic",
imageUrl = "files/organic.jpg"
image = Res.drawable.organic
),
SearchCategory(
name = "Gluten Free",
imageUrl = "files/gluten_free.jpg"
image = Res.drawable.gluten_free
),
SearchCategory(
name = "Paleo",
imageUrl = "files/paleo.jpg"
image = Res.drawable.paleo
),
SearchCategory(
name = "Vegan",
imageUrl = "files/vegan.jpg"
image = Res.drawable.vegan
),
SearchCategory(
name = "Vegitarian",
imageUrl = "files/grapes.jpg"
image = Res.drawable.organic
),
SearchCategory(
name = "Whole30",
imageUrl = "files/popcorn.jpg"
image = Res.drawable.paleo
)
)
)

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

@ -17,13 +17,18 @@
package com.example.jetsnack.model
import androidx.compose.runtime.Immutable
import com.example.common.generated.resources.*
import com.example.common.generated.resources.Res
import com.example.common.generated.resources.cupcake
import com.example.common.generated.resources.donut
import com.example.common.generated.resources.eclair
import org.jetbrains.compose.resources.DrawableResource
@Immutable
data class Snack(
val id: Long,
val name: String,
val imageUrl: String,
val image: DrawableResource,
val price: Long,
val tagline: String = "",
val tags: Set<String> = emptySet()
@ -38,190 +43,190 @@ val snacks = listOf(
id = 1L,
name = "Cupcake",
tagline = "A tag line",
imageUrl = "files/cupcake.jpg",
image = Res.drawable.cupcake,
price = 299
),
Snack(
id = 2L,
name = "Donut",
tagline = "A tag line",
imageUrl = "files/donut.jpg",
image = Res.drawable.donut,
price = 290
),
Snack(
id = 3L,
name = "Eclair",
tagline = "A tag line",
imageUrl = "files/eclair.jpg",
image = Res.drawable.eclair,
price = 289
),
Snack(
id = 4L,
name = "Froyo",
tagline = "A tag line",
imageUrl = "files/froyo.jpg",
image = Res.drawable.froyo,
price = 288
),
Snack(
id = 5L,
name = "Gingerbread",
tagline = "A tag line",
imageUrl = "files/gingerbread.jpg",
image = Res.drawable.gingerbread,
price = 499
),
Snack(
id = 6L,
name = "Honeycomb",
tagline = "A tag line",
imageUrl = "files/honeycomb.jpg",
image = Res.drawable.honeycomb,
price = 309
),
Snack(
id = 7L,
name = "Ice Cream Sandwich",
tagline = "A tag line",
imageUrl = "files/ice_cream_sandwich.jpg",
image = Res.drawable.ice_cream_sandwich,
price = 1299
),
Snack(
id = 8L,
name = "Jellybean",
tagline = "A tag line",
imageUrl = "files/jelly_bean.jpg",
image = Res.drawable.jelly_bean,
price = 109
),
Snack(
id = 9L,
name = "KitKat",
tagline = "A tag line",
imageUrl = "files/kitkat.jpg",
image = Res.drawable.kitkat,
price = 549
),
Snack(
id = 10L,
name = "Lollipop",
tagline = "A tag line",
imageUrl = "files/lollipop.jpg",
image = Res.drawable.lollipop,
price = 209
),
Snack(
id = 11L,
name = "Marshmallow",
tagline = "A tag line",
imageUrl = "files/marshmallow.jpg",
image = Res.drawable.marshmallow,
price = 219
),
Snack(
id = 12L,
name = "Nougat",
tagline = "A tag line",
imageUrl = "files/nougat.jpg",
image = Res.drawable.nougat,
price = 309
),
Snack(
id = 13L,
name = "Oreo",
tagline = "A tag line",
imageUrl = "files/oreo.jpg",
image = Res.drawable.oreo,
price = 339
),
Snack(
id = 14L,
name = "Pie",
tagline = "A tag line",
imageUrl = "files/pie.jpg",
image = Res.drawable.pie,
price = 249
),
Snack(
id = 15L,
name = "Chips",
imageUrl = "files/chips.jpg",
image = Res.drawable.chips,
price = 277
),
Snack(
id = 16L,
name = "Pretzels",
imageUrl = "files/pretzels.jpg",
image = Res.drawable.pretzels,
price = 154
),
Snack(
id = 17L,
name = "Smoothies",
imageUrl = "files/smoothies.jpg",
image = Res.drawable.smoothies,
price = 257
),
Snack(
id = 18L,
name = "Popcorn",
imageUrl = "files/popcorn.jpg",
image = Res.drawable.popcorn,
price = 167
),
Snack(
id = 19L,
name = "Almonds",
imageUrl = "files/almonds.jpg",
image = Res.drawable.almonds,
price = 123
),
Snack(
id = 20L,
name = "Cheese",
imageUrl = "files/cheese.jpg",
image = Res.drawable.cheese,
price = 231
),
Snack(
id = 21L,
name = "Apples",
tagline = "A tag line",
imageUrl = "files/apples.jpg",
image = Res.drawable.apples,
price = 221
),
Snack(
id = 22L,
name = "Apple sauce",
tagline = "A tag line",
imageUrl = "files/apple_sauce.jpg",
image = Res.drawable.apple_sauce,
price = 222
),
Snack(
id = 23L,
name = "Apple chips",
tagline = "A tag line",
imageUrl = "files/apple_chips.jpg",
image = Res.drawable.apple_chips,
price = 231
),
Snack(
id = 24L,
name = "Apple juice",
tagline = "A tag line",
imageUrl = "files/apple_juice.jpg",
image = Res.drawable.apple_juice,
price = 241
),
Snack(
id = 25L,
name = "Apple pie",
tagline = "A tag line",
imageUrl = "files/apple_pie.jpg",
image = Res.drawable.apple_pie,
price = 225
),
Snack(
id = 26L,
name = "Grapes",
tagline = "A tag line",
imageUrl = "files/grapes.jpg",
image = Res.drawable.grapes,
price = 266
),
Snack(
id = 27L,
name = "Kiwi",
tagline = "A tag line",
imageUrl = "files/kiwi.jpg",
image = Res.drawable.kiwi,
price = 127
),
Snack(
id = 28L,
name = "Mango",
tagline = "A tag line",
imageUrl = "files/mango.jpg",
image = Res.drawable.mango,
price = 128
)
)

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

@ -17,6 +17,7 @@
package com.example.jetsnack.model
import androidx.compose.runtime.Immutable
import kotlin.random.Random
@Immutable
data class SnackCollection(
@ -57,33 +58,33 @@ private val tastyTreats = SnackCollection(
)
private val popular = SnackCollection(
id = 2L,
id = Random.nextLong(),
name = "Popular on Jetsnack",
snacks = snacks.subList(14, 19)
)
private val wfhFavs = tastyTreats.copy(
id = 3L,
id = Random.nextLong(),
name = "WFH favourites"
)
private val newlyAdded = popular.copy(
id = 4L,
id = Random.nextLong(),
name = "Newly Added"
)
private val exclusive = tastyTreats.copy(
id = 5L,
id = Random.nextLong(),
name = "Only on Jetsnack"
)
private val also = tastyTreats.copy(
id = 6L,
id = Random.nextLong(),
name = "Customers also bought"
)
private val inspiredByCart = tastyTreats.copy(
id = 7L,
id = Random.nextLong(),
name = "Inspired by your cart"
)
@ -96,8 +97,8 @@ private val snackCollections = listOf(
)
private val related = listOf(
also,
popular
also.copy(id = Random.nextLong()),
popular.copy(id = Random.nextLong())
)
private val cart = listOf(

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

@ -20,8 +20,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import org.jetbrains.compose.resources.StringResource
data class Message(val id: Long, val message: Int /*@StringRes*/)
data class Message(val id: Long, val messageId: StringResource)
/**
* Class responsible for managing Snackbar messages to show on the screen
@ -31,11 +32,11 @@ object SnackbarManager {
private val _messages: MutableStateFlow<List<Message>> = MutableStateFlow(emptyList())
val messages: StateFlow<List<Message>> get() = _messages.asStateFlow()
fun showMessage(message: Int /*@StringRes*/) {
fun showMessage(message: StringResource) {
_messages.update { currentMessages ->
currentMessages + Message(
id = createRandomUUID(),
message = message
messageId = message
)
}
}

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

@ -16,44 +16,149 @@
package com.example.jetsnack.ui
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.SnackbarHost
import androidx.compose.animation.*
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.SnackbarHost
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.navArgument
import com.example.jetsnack.ui.components.JetsnackScaffold
import com.example.jetsnack.ui.components.JetsnackSnackbar
import com.example.jetsnack.ui.components.rememberJetsnackScaffoldState
import com.example.jetsnack.ui.home.HomeSections
import com.example.jetsnack.ui.home.JetsnackBottomBar
import com.example.jetsnack.ui.snackdetail.jetSnackSystemBarsPadding
import com.example.jetsnack.ui.home.addHomeGraph
import com.example.jetsnack.ui.home.composableWithCompositionLocal
import com.example.jetsnack.ui.navigation.MainDestinations
import com.example.jetsnack.ui.navigation.rememberJetsnackNavController
import com.example.jetsnack.ui.snackdetail.SnackDetail
import com.example.jetsnack.ui.snackdetail.nonSpatialExpressiveSpring
import com.example.jetsnack.ui.snackdetail.spatialExpressiveSpring
import com.example.jetsnack.ui.theme.JetsnackTheme
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun JetsnackApp() {
JetsnackTheme {
val appState = rememberMppJetsnackAppState()
JetsnackScaffold(
bottomBar = {
if (appState.shouldShowBottomBar()) {
val jetsnackNavController = rememberJetsnackNavController()
SharedTransitionLayout {
CompositionLocalProvider(
LocalSharedTransitionScope provides this
) {
NavHost(
navController = jetsnackNavController.navController,
startDestination = MainDestinations.HOME_ROUTE
) {
composableWithCompositionLocal(
route = MainDestinations.HOME_ROUTE
) { backStackEntry ->
MainContainer(
onSnackSelected = jetsnackNavController::navigateToSnackDetail
)
}
composableWithCompositionLocal(
"${MainDestinations.SNACK_DETAIL_ROUTE}/" +
"{${MainDestinations.SNACK_ID_KEY}}" +
"?origin={${MainDestinations.ORIGIN}}",
arguments = listOf(
navArgument(MainDestinations.SNACK_ID_KEY) {
type = NavType.LongType
}
),
) { backStackEntry ->
val arguments = requireNotNull(backStackEntry.arguments)
val snackId = arguments.getLong(MainDestinations.SNACK_ID_KEY)
val origin = arguments.getString(MainDestinations.ORIGIN)
SnackDetail(
snackId,
origin = origin ?: "",
upPress = jetsnackNavController::upPress
)
}
}
}
}
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun MainContainer(
modifier: Modifier = Modifier,
onSnackSelected: (Long, String, NavBackStackEntry) -> Unit
) {
val jetsnackScaffoldState = rememberJetsnackScaffoldState()
val nestedNavController = rememberJetsnackNavController()
val navBackStackEntry by nestedNavController.navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
val sharedTransitionScope = LocalSharedTransitionScope.current
?: throw IllegalStateException("No SharedElementScope found")
val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current
?: throw IllegalStateException("No SharedElementScope found")
JetsnackScaffold(
bottomBar = {
with(animatedVisibilityScope) {
with(sharedTransitionScope) {
JetsnackBottomBar(
tabs = appState.bottomBarTabs,
currentRoute = appState.currentRoute!!,
navigateToRoute = appState::navigateToBottomBarRoute
tabs = HomeSections.entries.toTypedArray(),
currentRoute = currentRoute ?: HomeSections.FEED.route,
navigateToRoute = nestedNavController::navigateToBottomBarRoute,
modifier = Modifier
.renderInSharedTransitionScopeOverlay(
zIndexInOverlay = 1f,
)
.animateEnterExit(
enter = fadeIn(nonSpatialExpressiveSpring()) + slideInVertically(
spatialExpressiveSpring()
) {
it
},
exit = fadeOut(nonSpatialExpressiveSpring()) + slideOutVertically(
spatialExpressiveSpring()
) {
it
}
)
)
}
},
snackbarHost = {
SnackbarHost(
hostState = it,
modifier = Modifier.jetSnackSystemBarsPadding(),
snackbar = { snackbarData -> JetsnackSnackbar(snackbarData) }
)
},
scaffoldState = appState.scaffoldState
) { innerPaddingModifier ->
JetsnackScaffoldContent(innerPaddingModifier, appState)
}
},
modifier = modifier,
snackbarHost = {
SnackbarHost(
hostState = it,
modifier = Modifier.systemBarsPadding(),
snackbar = { snackbarData -> JetsnackSnackbar(snackbarData) }
)
},
snackBarHostState = jetsnackScaffoldState.snackBarHostState,
) { padding ->
NavHost(
navController = nestedNavController.navController,
startDestination = HomeSections.FEED.route
) {
addHomeGraph(
onSnackSelected = onSnackSelected,
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
)
}
}
}
@Composable
expect fun JetsnackScaffoldContent(innerPaddingModifier: PaddingValues, appState: MppJetsnackAppState)
val LocalNavAnimatedVisibilityScope = compositionLocalOf<AnimatedVisibilityScope?> { null }
@OptIn(ExperimentalSharedTransitionApi::class)
val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { null }

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

@ -1,166 +0,0 @@
/*
* 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
//}

22
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/SystemUi.kt → examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/SnackSharedElementKey.kt

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Android Open Source Project
* Copyright 2024 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.
@ -14,8 +14,20 @@
* limitations under the License.
*/
package com.example.jetsnack.ui.utils
package com.example.jetsnack.ui
/**
* Moved to https://google.github.io/accompanist/systemuicontroller/
*/
data class SnackSharedElementKey(
val snackId: Long,
val origin: String,
val type: SnackSharedElementType
)
enum class SnackSharedElementType {
Bounds,
Image,
Title,
Tagline,
Background
}
object FilterSharedElementKey

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

@ -16,7 +16,6 @@
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
@ -29,11 +28,10 @@ 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.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -41,13 +39,11 @@ 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,
@ -83,18 +79,15 @@ fun JetsnackButton(
)
) {
ProvideTextStyle(
value = MaterialTheme.typography.button
value = MaterialTheme.typography.labelLarge
) {
Row(
@Suppress("DEPRECATION_ERROR")
Modifier
.defaultMinSize(
minWidth = ButtonDefaults.MinWidth,
minHeight = ButtonDefaults.MinHeight
)
// TODO This should be replaced by non-deprecated alternative after the original example migrates to Jetpack Compose 1.7:
// https://github.com/android/compose-samples/blob/3bc6b7d7c74571ea74776ec5b15518b40de4d31b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt#L95
.indication(interactionSource, rememberRipple())
.indication(interactionSource, ripple())
.padding(contentPadding),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
@ -104,26 +97,4 @@ fun JetsnackButton(
}
}
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")
}
}
}
private val ButtonShape = RoundedCornerShape(percent = 50)

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

@ -16,11 +16,8 @@
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.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -48,14 +45,4 @@ fun JetsnackCard(
border = border,
content = content
)
}
//@Preview
@Composable
private fun CardPreview() {
JetsnackTheme {
JetsnackCard {
Text(text = "Demo", modifier = Modifier.padding(16.dp))
}
}
}
}

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

@ -16,12 +16,8 @@
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.material3.HorizontalDivider
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
@ -32,25 +28,13 @@ import com.example.jetsnack.ui.theme.JetsnackTheme
fun JetsnackDivider(
modifier: Modifier = Modifier,
color: Color = JetsnackTheme.colors.uiBorder.copy(alpha = DividerAlpha),
thickness: Dp = 1.dp,
startIndent: Dp = 0.dp
thickness: Dp = 1.dp
) {
Divider(
HorizontalDivider(
modifier = modifier,
color = color,
thickness = thickness,
startIndent = startIndent
thickness = thickness
)
}
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))
}
}
}

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

@ -16,26 +16,24 @@
package com.example.jetsnack.ui.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
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.layout.*
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.material.icons.rounded.FilterList
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@ -44,40 +42,54 @@ 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.common.generated.resources.Res
import com.example.common.generated.resources.label_filters
import com.example.jetsnack.model.Filter
import com.example.jetsnack.stringResource
import com.example.jetsnack.ui.FilterSharedElementKey
import com.example.jetsnack.ui.theme.JetsnackTheme
import org.jetbrains.compose.resources.stringResource
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun FilterBar(
filters: List<Filter>,
onShowFilters: () -> Unit
onShowFilters: () -> Unit,
filterScreenVisible: Boolean,
sharedTransitionScope: SharedTransitionScope
) {
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
)
)
with(sharedTransitionScope) {
LazyRow(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(start = 12.dp, end = 8.dp),
modifier = Modifier.heightIn(min = 56.dp)
) {
item {
AnimatedVisibility(visible = !filterScreenVisible) {
IconButton(
onClick = onShowFilters,
modifier = Modifier
.sharedBounds(
rememberSharedContentState(FilterSharedElementKey),
animatedVisibilityScope = this@AnimatedVisibility,
resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds
)
) {
Icon(
imageVector = Icons.Rounded.FilterList,
tint = JetsnackTheme.colors.brand,
contentDescription = stringResource(Res.string.label_filters),
modifier = Modifier.diagonalGradientBorder(
colors = JetsnackTheme.colors.interactiveSecondary,
shape = CircleShape
)
)
}
}
}
items(filters) { filter ->
FilterChip(filter = filter, shape = MaterialTheme.shapes.small)
}
}
items(filters) { filter ->
FilterChip(filter = filter, shape = MaterialTheme.shapes.small)
}
}
}
@ -90,7 +102,8 @@ fun FilterChip(
) {
val (selected, setSelected) = filter.enabled
val backgroundColor by animateColorAsState(
if (selected) JetsnackTheme.colors.brandSecondary else JetsnackTheme.colors.uiBackground
if (selected) JetsnackTheme.colors.brandSecondary else JetsnackTheme.colors.uiBackground,
label = "background color"
)
val border = Modifier.fadeInDiagonalGradientBorder(
showBorder = !selected,
@ -98,11 +111,12 @@ fun FilterChip(
shape = shape
)
val textColor by animateColorAsState(
if (selected) Color.Black else JetsnackTheme.colors.textSecondary
if (selected) Color.Black else JetsnackTheme.colors.textSecondary,
label = "text color"
)
JetsnackSurface(
modifier = modifier.height(28.dp),
modifier = modifier,
color = backgroundColor,
contentColor = textColor,
shape = shape,
@ -134,7 +148,7 @@ fun FilterChip(
) {
Text(
text = filter.name,
style = MaterialTheme.typography.caption,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
modifier = Modifier.padding(
horizontal = 20.dp,
@ -144,19 +158,3 @@ fun FilterChip(
}
}
}
//@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))
}
}

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

@ -21,12 +21,14 @@ 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.drawBehind
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.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@ -47,13 +49,30 @@ fun Modifier.offsetGradientBackground(
offset: Float = 0f
) = background(
Brush.horizontalGradient(
colors,
colors = colors,
startX = -offset,
endX = width - offset,
tileMode = TileMode.Mirror
)
)
fun Modifier.offsetGradientBackground(
colors: List<Color>,
width: Density.() -> Float,
offset: Density.() -> Float = { 0f }
) = drawBehind {
val actualOffset = offset()
drawRect(
Brush.horizontalGradient(
colors = colors,
startX = -actualOffset,
endX = width() - actualOffset,
tileMode = TileMode.Mirror
)
)
}
fun Modifier.diagonalGradientBorder(
colors: List<Color>,
borderSize: Dp = 2.dp,
@ -71,11 +90,14 @@ fun Modifier.fadeInDiagonalGradientBorder(
shape: Shape
) = composed {
val animatedColors = List(colors.size) { i ->
animateColorAsState(if (showBorder) colors[i] else colors[i].copy(alpha = 0f)).value
animateColorAsState(
if (showBorder) colors[i] else colors[i].copy(alpha = 0f),
label = "animated color"
).value
}
diagonalGradientBorder(
colors = animatedColors,
borderSize = borderSize,
shape = shape
)
}
}

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

@ -23,10 +23,8 @@ 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.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@ -99,11 +97,11 @@ fun JetsnackGradientTintedIconButton(
@Composable
private fun GradientTintedIconButtonPreview() {
JetsnackTheme {
JetsnackGradientTintedIconButton(
imageVector = Icons.Default.Add,
onClick = {},
contentDescription = "Demo",
modifier = Modifier.padding(4.dp)
)
// JetsnackGradientTintedIconButton(
// imageVector = Icons.Default.Add,
// onClick = {},
// contentDescription = "Demo",
// modifier = Modifier.padding(4.dp)
// )
}
}

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

@ -17,27 +17,27 @@
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.material.icons.filled.Remove
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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.font.FontWeight
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.common.generated.resources.Res
import com.example.common.generated.resources.label_decrease
import com.example.common.generated.resources.label_increase
import com.example.common.generated.resources.quantity
import com.example.jetsnack.ui.theme.JetsnackTheme
import org.jetbrains.compose.resources.stringResource
@Composable
fun QuantitySelector(
@ -47,20 +47,19 @@ fun QuantitySelector(
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)
)
}
Text(
text = stringResource(Res.string.quantity),
style = MaterialTheme.typography.titleMedium,
color = JetsnackTheme.colors.textSecondary,
fontWeight = FontWeight.Normal,
modifier = Modifier
.padding(end = 18.dp)
.align(Alignment.CenterVertically)
)
JetsnackGradientTintedIconButton(
imageVector = Icons.Default.Remove,
onClick = decreaseItemCount,
contentDescription = stringResource(MppR.string.label_decrease),
contentDescription = stringResource(Res.string.label_decrease),
modifier = Modifier.align(Alignment.CenterVertically)
)
Crossfade(
@ -70,7 +69,7 @@ fun QuantitySelector(
) {
Text(
text = "$it",
style = MaterialTheme.typography.subtitle2,
style = MaterialTheme.typography.titleSmall,
fontSize = 18.sp,
color = JetsnackTheme.colors.textPrimary,
textAlign = TextAlign.Center,
@ -80,30 +79,8 @@ fun QuantitySelector(
JetsnackGradientTintedIconButton(
imageVector = Icons.Default.Add,
onClick = increaseItemCount,
contentDescription = stringResource(MppR.string.label_increase),
contentDescription = stringResource(Res.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, {}, {})
}
}
}
}
}

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

@ -16,65 +16,89 @@
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.material3.FabPosition
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.model.SnackbarManager
import com.example.jetsnack.ui.theme.JetsnackTheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
/**
* Wrap Material [androidx.compose.material.Scaffold] and set [JetsnackTheme] colors.
* Wrap Material [androidx.compose.material3.Scaffold] and set [JetsnackTheme] colors.
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun JetsnackScaffold(
modifier: Modifier = Modifier,
scaffoldState: ScaffoldState = rememberScaffoldState(),
snackBarHostState: SnackbarHostState = remember { SnackbarHostState() },
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,
snackbarHost = {
snackbarHost(snackBarHostState)
},
floatingActionButton = floatingActionButton,
floatingActionButtonPosition = floatingActionButtonPosition,
isFloatingActionButtonDocked = isFloatingActionButtonDocked,
drawerContent = drawerContent,
drawerShape = drawerShape,
drawerElevation = drawerElevation,
drawerBackgroundColor = drawerBackgroundColor,
drawerContentColor = drawerContentColor,
drawerScrimColor = drawerScrimColor,
backgroundColor = backgroundColor,
containerColor = backgroundColor,
contentColor = contentColor,
content = content
)
}
/**
* Remember and creates an instance of [JetsnackScaffoldState]
*/
@Composable
fun rememberJetsnackScaffoldState(
snackBarHostState: SnackbarHostState = remember { SnackbarHostState() },
snackbarManager: SnackbarManager = SnackbarManager,
coroutineScope: CoroutineScope = rememberCoroutineScope()
): JetsnackScaffoldState = remember(snackBarHostState, snackbarManager, coroutineScope) {
JetsnackScaffoldState(snackBarHostState, snackbarManager, coroutineScope)
}
/**
* Responsible for holding [ScaffoldState], handles the logic of showing snackbar messages
*/
@Stable
class JetsnackScaffoldState(
val snackBarHostState: SnackbarHostState,
private val snackbarManager: SnackbarManager,
coroutineScope: CoroutineScope
) {
// Process snackbars coming from SnackbarManager
init {
coroutineScope.launch {
snackbarManager.messages.collect { currentMessages ->
if (currentMessages.isNotEmpty()) {
val message = currentMessages[0]
val text = currentMessages.toString()//stringResource(message.messageId)
// Notify the SnackbarManager so it can remove the current message from the list
snackbarManager.setMessageShown(message.id)
// Display the snackbar on the screen. `showSnackbar` is a function
// that suspends until the snackbar disappears from the screen
snackBarHostState.showSnackbar(text.toString())
}
}
}
}
}

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

@ -16,19 +16,17 @@
package com.example.jetsnack.ui.components
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Snackbar
import androidx.compose.material.SnackbarData
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Snackbar
import androidx.compose.material3.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
* An alternative to [androidx.compose.material3.Snackbar] utilizing
* [com.example.jetsnack.ui.theme.JetsnackColors]
*/
@Composable
@ -39,17 +37,15 @@ fun JetsnackSnackbar(
shape: Shape = MaterialTheme.shapes.small,
backgroundColor: Color = JetsnackTheme.colors.uiBackground,
contentColor: Color = JetsnackTheme.colors.textSecondary,
actionColor: Color = JetsnackTheme.colors.brand,
elevation: Dp = 6.dp
actionColor: Color = JetsnackTheme.colors.brand
) {
Snackbar(
snackbarData = snackbarData,
modifier = modifier,
actionOnNewLine = actionOnNewLine,
shape = shape,
backgroundColor = backgroundColor,
containerColor = backgroundColor,
contentColor = contentColor,
actionColor = actionColor,
elevation = elevation
actionColor = actionColor
)
}
}

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

@ -17,49 +17,53 @@
package com.example.jetsnack.ui.components
//import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.animation.*
import androidx.compose.animation.core.animateDp
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
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.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
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.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.ArrowForward
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Density
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.LocalNavAnimatedVisibilityScope
import com.example.jetsnack.ui.LocalSharedTransitionScope
import com.example.jetsnack.ui.SnackSharedElementKey
import com.example.jetsnack.ui.SnackSharedElementType
import com.example.jetsnack.ui.snackdetail.nonSpatialExpressiveSpring
import com.example.jetsnack.ui.snackdetail.snackDetailBoundsTransform
import com.example.jetsnack.ui.theme.JetsnackTheme
import com.example.jetsnack.ui.utils.mirroringIcon
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.imageResource
private val HighlightCardWidth = 170.dp
private val HighlightCardPadding = 16.dp
private val Density.cardWidthWithPaddingPx
get() = (HighlightCardWidth + HighlightCardPadding).toPx()
// The Cards show a gradient which spans 3 cards and scrolls with parallax.
private val gradientWidth
@ -71,7 +75,7 @@ private val gradientWidth
@Composable
fun SnackCollection(
snackCollection: SnackCollection,
onSnackClick: (Long) -> Unit,
onSnackClick: (Long, String) -> Unit,
modifier: Modifier = Modifier,
index: Int = 0,
highlight: Boolean = true
@ -85,7 +89,7 @@ fun SnackCollection(
) {
Text(
text = snackCollection.name,
style = MaterialTheme.typography.h6,
style = MaterialTheme.typography.titleLarge,
color = JetsnackTheme.colors.brand,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -98,52 +102,56 @@ fun SnackCollection(
modifier = Modifier.align(Alignment.CenterVertically)
) {
Icon(
imageVector = mirroringIcon(
ltrIcon = Icons.Outlined.ArrowForward,
rtlIcon = Icons.Outlined.ArrowBack
),
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
tint = JetsnackTheme.colors.brand,
contentDescription = null
)
}
}
if (highlight && snackCollection.type == CollectionType.Highlight) {
HighlightedSnacks(index, snackCollection.snacks, onSnackClick)
HighlightedSnacks(snackCollection.id, index, snackCollection.snacks, onSnackClick)
} else {
Snacks(snackCollection.snacks, onSnackClick)
Snacks(snackCollection.id, snackCollection.snacks, onSnackClick)
}
}
}
@Composable
private fun HighlightedSnacks(
snackCollectionId: Long,
index: Int,
snacks: List<Snack>,
onSnackClick: (Long) -> Unit,
onSnackClick: (Long, String) -> Unit,
modifier: Modifier = Modifier
) {
val scroll = rememberScrollState(0)
val rowState = rememberLazyListState()
val cardWidthWithPaddingPx = with(LocalDensity.current) { cardWidthWithPaddingPx }
val scrollProvider = {
// Simple calculation of scroll distance for homogenous item types with the same width.
val offsetFromStart = cardWidthWithPaddingPx * rowState.firstVisibleItemIndex
offsetFromStart + rowState.firstVisibleItemScrollOffset
}
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(
state = rowState,
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
snackCollectionId = snackCollectionId,
snack = snack,
onSnackClick = onSnackClick,
index = index,
gradient = gradient,
scrollProvider = scrollProvider
)
}
}
@ -151,8 +159,9 @@ private fun HighlightedSnacks(
@Composable
private fun Snacks(
snackCollectionId: Long,
snacks: List<Snack>,
onSnackClick: (Long) -> Unit,
onSnackClick: (Long, String) -> Unit,
modifier: Modifier = Modifier
) {
LazyRow(
@ -160,15 +169,17 @@ private fun Snacks(
contentPadding = PaddingValues(start = 12.dp, end = 12.dp)
) {
items(snacks) { snack ->
SnackItem(snack, onSnackClick)
SnackItem(snack, snackCollectionId, onSnackClick)
}
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun SnackItem(
snack: Snack,
onSnackClick: (Long) -> Unit,
snackCollectionId: Long,
onSnackClick: (Long, String) -> Unit,
modifier: Modifier = Modifier
) {
JetsnackSurface(
@ -178,98 +189,253 @@ fun SnackItem(
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)
)
val sharedTransitionScope = LocalSharedTransitionScope.current
?: throw IllegalStateException("No sharedTransitionScope found")
val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current
?: throw IllegalStateException("No animatedVisibilityScope found")
with(sharedTransitionScope) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.clickable(onClick = {
onSnackClick(snack.id, snackCollectionId.toString())
})
.padding(8.dp)
) {
SnackImage(
image = snack.image,
elevation = 1.dp,
contentDescription = null,
modifier = Modifier
.size(120.dp)
.sharedBounds(
rememberSharedContentState(
key = SnackSharedElementKey(
snackId = snack.id,
origin = snackCollectionId.toString(),
type = SnackSharedElementType.Image
)
),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = snackDetailBoundsTransform
)
)
Text(
text = snack.name,
style = MaterialTheme.typography.titleMedium,
color = JetsnackTheme.colors.textSecondary,
modifier = Modifier
.padding(top = 8.dp)
.wrapContentWidth()
.sharedBounds(
rememberSharedContentState(
key = SnackSharedElementKey(
snackId = snack.id,
origin = snackCollectionId.toString(),
type = SnackSharedElementType.Title
)
),
animatedVisibilityScope = animatedVisibilityScope,
enter = fadeIn(nonSpatialExpressiveSpring()),
exit = fadeOut(nonSpatialExpressiveSpring()),
resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds(),
boundsTransform = snackDetailBoundsTransform
)
)
}
}
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun HighlightSnackItem(
snackCollectionId: Long,
snack: Snack,
onSnackClick: (Long) -> Unit,
onSnackClick: (Long, String) -> Unit,
index: Int,
gradient: List<Color>,
gradientWidth: Float,
scroll: Int,
scrollProvider: () -> Float,
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()
val sharedTransitionScope = LocalSharedTransitionScope.current
?: throw IllegalStateException("No Scope found")
val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current
?: throw IllegalStateException("No Scope found")
with(sharedTransitionScope) {
val roundedCornerAnimation by animatedVisibilityScope.transition
.animateDp(label = "rounded corner") { enterExit: EnterExitState ->
when (enterExit) {
EnterExitState.PreEnter -> 0.dp
EnterExitState.Visible -> 20.dp
EnterExitState.PostExit -> 20.dp
}
}
JetsnackCard(
elevation = 0.dp,
shape = RoundedCornerShape(roundedCornerAnimation),
modifier = modifier
.padding(bottom = 16.dp)
.sharedBounds(
sharedContentState = rememberSharedContentState(
key = SnackSharedElementKey(
snackId = snack.id,
origin = snackCollectionId.toString(),
type = SnackSharedElementType.Bounds
)
),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = snackDetailBoundsTransform,
clipInOverlayDuringTransition = OverlayClip(
RoundedCornerShape(
roundedCornerAnimation
)
),
enter = fadeIn(),
exit = fadeOut()
)
.size(
width = HighlightCardWidth,
height = 250.dp
)
.border(
1.dp,
JetsnackTheme.colors.uiBorder.copy(alpha = 0.12f),
RoundedCornerShape(roundedCornerAnimation)
)
) {
Box(
Column(
modifier = Modifier
.height(160.dp)
.fillMaxWidth()
.clickable(onClick = {
onSnackClick(
snack.id,
snackCollectionId.toString()
)
})
.fillMaxSize()
) {
val gradientOffset = left - (scroll / 3f)
Box(
modifier = Modifier
.height(100.dp)
.height(160.dp)
.fillMaxWidth()
.offsetGradientBackground(gradient, gradientWidth, gradientOffset)
) {
Box(
modifier = Modifier
.sharedBounds(
rememberSharedContentState(
key = SnackSharedElementKey(
snackId = snack.id,
origin = snackCollectionId.toString(),
type = SnackSharedElementType.Background
)
),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = snackDetailBoundsTransform,
enter = fadeIn(nonSpatialExpressiveSpring()),
exit = fadeOut(nonSpatialExpressiveSpring()),
resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds()
)
.height(100.dp)
.fillMaxWidth()
.offsetGradientBackground(
colors = gradient,
width = {
// The Cards show a gradient which spans 6 cards and
// scrolls with parallax.
6 * cardWidthWithPaddingPx
},
offset = {
val left = index * cardWidthWithPaddingPx
val gradientOffset = left - (scrollProvider() / 3f)
gradientOffset
}
)
)
SnackImage(
image = snack.image,
contentDescription = null,
modifier = Modifier
.sharedBounds(
rememberSharedContentState(
key = SnackSharedElementKey(
snackId = snack.id,
origin = snackCollectionId.toString(),
type = SnackSharedElementType.Image
)
),
animatedVisibilityScope = animatedVisibilityScope,
exit = fadeOut(nonSpatialExpressiveSpring()),
enter = fadeIn(nonSpatialExpressiveSpring()),
boundsTransform = snackDetailBoundsTransform
)
.align(Alignment.BottomCenter)
.size(120.dp)
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = snack.name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleLarge,
color = JetsnackTheme.colors.textSecondary,
modifier = Modifier
.padding(horizontal = 16.dp)
.sharedBounds(
rememberSharedContentState(
key = SnackSharedElementKey(
snackId = snack.id,
origin = snackCollectionId.toString(),
type = SnackSharedElementType.Title
)
),
animatedVisibilityScope = animatedVisibilityScope,
enter = fadeIn(nonSpatialExpressiveSpring()),
exit = fadeOut(nonSpatialExpressiveSpring()),
boundsTransform = snackDetailBoundsTransform,
resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds()
)
.wrapContentWidth()
)
SnackImage(
imageUrl = snack.imageUrl,
contentDescription = null,
Spacer(modifier = Modifier.height(4.dp))
Text(
text = snack.tagline,
style = MaterialTheme.typography.bodyLarge,
color = JetsnackTheme.colors.textHelp,
modifier = Modifier
.size(120.dp)
.align(Alignment.BottomCenter)
.padding(horizontal = 16.dp)
.sharedBounds(
rememberSharedContentState(
key = SnackSharedElementKey(
snackId = snack.id,
origin = snackCollectionId.toString(),
type = SnackSharedElementType.Tagline
)
),
animatedVisibilityScope = animatedVisibilityScope,
enter = fadeIn(nonSpatialExpressiveSpring()),
exit = fadeOut(nonSpatialExpressiveSpring()),
boundsTransform = snackDetailBoundsTransform,
resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds()
)
.wrapContentWidth()
)
}
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,
image: DrawableResource,
contentDescription: String?,
modifier: Modifier = Modifier,
elevation: Dp = 0.dp
@ -280,29 +446,11 @@ fun SnackImage(
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
Image(
bitmap = imageResource(image),
contentDescription = contentDescription,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
}
}
}

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

@ -20,7 +20,7 @@ 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.material3.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
@ -37,7 +37,7 @@ import com.example.jetsnack.ui.theme.JetsnackTheme
import kotlin.math.ln
/**
* An alternative to [androidx.compose.material.Surface] utilizing
* An alternative to [androidx.compose.material3.Surface] utilizing
* [com.example.jetsnack.ui.theme.JetsnackColors]
*/
@Composable
@ -51,7 +51,8 @@ fun JetsnackSurface(
content: @Composable () -> Unit
) {
Box(
modifier = modifier.shadow(elevation = elevation, shape = shape, clip = false)
modifier = modifier
.shadow(elevation = elevation, shape = shape, clip = false)
.zIndex(elevation.value)
.then(if (border != null) Modifier.border(border, shape) else Modifier)
.background(
@ -67,8 +68,8 @@ fun JetsnackSurface(
@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
// JetsnackTheme.colors.isDark //&&
// color == JetsnackTheme.colors.uiBackground
) {
color.withElevation(elevation)
} else {

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

@ -16,67 +16,82 @@
package com.example.jetsnack.ui.home
//import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
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.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.material3.*
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.common.generated.resources.Res
import com.example.common.generated.resources.label_select_delivery
import com.example.jetsnack.ui.LocalNavAnimatedVisibilityScope
import com.example.jetsnack.ui.LocalSharedTransitionScope
import com.example.jetsnack.ui.components.JetsnackDivider
import com.example.jetsnack.ui.snackdetail.jetSnackStatusBarsPadding
import com.example.jetsnack.ui.snackdetail.spatialExpressiveSpring
import com.example.jetsnack.ui.theme.AlphaNearOpaque
import com.example.jetsnack.ui.theme.JetsnackTheme
import org.jetbrains.compose.resources.stringResource
@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)
@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)
val sharedElementScope =
LocalSharedTransitionScope.current ?: throw IllegalStateException("No shared element scope")
val navAnimatedScope =
LocalNavAnimatedVisibilityScope.current ?: throw IllegalStateException("No nav scope")
with(sharedElementScope) {
with(navAnimatedScope) {
Column(
modifier = modifier
.renderInSharedTransitionScopeOverlay()
.animateEnterExit(
enter = slideInVertically(spatialExpressiveSpring()) { -it * 2 },
exit = slideOutVertically(spatialExpressiveSpring()) { -it * 2 }
)
) {
Icon(
imageVector = Icons.Outlined.ExpandMore,
tint = JetsnackTheme.colors.brand,
contentDescription = stringResource(MppR.string.label_select_delivery)
TopAppBar(
windowInsets = WindowInsets(0, 0, 0, 0),
title = {
Row {
Text(
text = "Huidekoperstraat 26-28, 1017 ZM Amsterdam | https://kotl.in/wasm-gio23",
style = MaterialTheme.typography.titleMedium,
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(Res.string.label_select_delivery)
)
}
}
},
colors = TopAppBarDefaults.topAppBarColors().copy(
containerColor = JetsnackTheme.colors.uiBackground
.copy(alpha = AlphaNearOpaque),
titleContentColor = JetsnackTheme.colors.textSecondary
),
)
JetsnackDivider()
}
}
JetsnackDivider()
}
}
//@Preview
@Composable
fun PreviewDestinationBar() {
JetsnackTheme {
DestinationBar()
}
}

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

@ -17,19 +17,17 @@
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.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
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.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.add
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
@ -37,9 +35,7 @@ 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
@ -49,12 +45,11 @@ 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.snackdetail.jetSnackStatusBarsPadding
import com.example.jetsnack.ui.theme.JetsnackTheme
@Composable
fun Feed(
onSnackClick: (Long) -> Unit,
onSnackClick: (Long, String) -> Unit,
modifier: Modifier = Modifier
) {
val snackCollections = remember { SnackRepo.getSnacks() }
@ -67,70 +62,77 @@ fun Feed(
)
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun Feed(
snackCollections: List<SnackCollection>,
filters: List<Filter>,
onSnackClick: (Long) -> Unit,
onSnackClick: (Long, String) -> Unit,
modifier: Modifier = Modifier
) {
JetsnackSurface(modifier = modifier.fillMaxSize()) {
Column {
DestinationBar(modifier = Modifier.jetSnackStatusBarsPadding())
SnackCollectionList(snackCollections, filters, onSnackClick)
var filtersVisible by remember {
mutableStateOf(false)
}
SharedTransitionLayout {
Box {
SnackCollectionList(
snackCollections,
filters,
filtersVisible = filtersVisible,
onFiltersSelected = {
filtersVisible = true
},
sharedTransitionScope = this@SharedTransitionLayout,
onSnackClick = onSnackClick
)
DestinationBar()
AnimatedVisibility(filtersVisible, enter = fadeIn(), exit = fadeOut()) {
FilterScreen(
animatedVisibilityScope = this@AnimatedVisibility,
sharedTransitionScope = this@SharedTransitionLayout
) { filtersVisible = false }
}
}
}
}
}
@OptIn(ExperimentalAnimationApi::class)
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun SnackCollectionList(
snackCollections: List<SnackCollection>,
filters: List<Filter>,
onSnackClick: (Long) -> Unit,
filtersVisible: Boolean,
onFiltersSelected: () -> Unit,
onSnackClick: (Long, String) -> Unit,
sharedTransitionScope: SharedTransitionScope,
modifier: Modifier = Modifier
) {
var filtersVisible by rememberSaveable { mutableStateOf(false) }
Box(modifier) {
LazyColumn {
item {
FilterBar(filters, onShowFilters = { filtersVisible = true })
}
itemsIndexed(snackCollections) { index, snackCollection ->
if (index > 0) {
JetsnackDivider(thickness = 2.dp)
}
SnackCollection(
snackCollection = snackCollection,
onSnackClick = onSnackClick,
index = index
LazyColumn(modifier = modifier) {
item {
Spacer(
Modifier.windowInsetsTopHeight(
WindowInsets.statusBars.add(WindowInsets(top = 56.dp))
)
}
)
FilterBar(
filters,
sharedTransitionScope = sharedTransitionScope,
filterScreenVisible = filtersVisible,
onShowFilters = onFiltersSelected
)
}
}
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
itemsIndexed(snackCollections) { index, snackCollection ->
if (index > 0) {
JetsnackDivider(thickness = 2.dp)
}
//@Preview
@Composable
fun HomePreview() {
JetsnackTheme {
Feed(onSnackClick = { })
SnackCollection(
snackCollection = snackCollection,
onSnackClick = onSnackClick,
index = index
)
}
}
}
}

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

@ -16,108 +16,137 @@
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.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
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.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
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.common.generated.resources.*
import com.example.common.generated.resources.Res
import com.example.common.generated.resources.close
import com.example.common.generated.resources.label_filters
import com.example.common.generated.resources.reset
import com.example.jetsnack.model.Filter
import com.example.jetsnack.model.SnackRepo
import com.example.jetsnack.ui.FilterSharedElementKey
import com.example.jetsnack.ui.components.FilterChip
import com.example.jetsnack.ui.components.JetsnackScaffold
import com.example.jetsnack.ui.theme.JetsnackTheme
import org.jetbrains.compose.resources.stringResource
@OptIn(ExperimentalAnimationApi::class)
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun FilterScreen(
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope,
onDismiss: () -> Unit
) {
var sortState by remember { mutableStateOf(SnackRepo.getSortDefault()) }
var maxCalories by remember { mutableStateOf(0f) }
var maxCalories by remember { mutableFloatStateOf(0f) }
val defaultFilter = SnackRepo.getSortDefault()
SnackDialog(onCloseRequest = onDismiss) {
Box(
modifier = Modifier
.fillMaxSize()
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
// capture click
}
) {
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
)
}
) {
Spacer(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.5f))
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
onDismiss()
}
)
with(sharedTransitionScope) {
Column(
Modifier
.fillMaxSize()
.padding(16.dp)
.align(Alignment.Center)
.clip(MaterialTheme.shapes.medium)
.sharedBounds(
rememberSharedContentState(FilterSharedElementKey),
animatedVisibilityScope = animatedVisibilityScope,
resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds,
clipInOverlayDuringTransition = OverlayClip(MaterialTheme.shapes.medium)
)
.wrapContentSize()
.heightIn(max = 450.dp)
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp, vertical = 16.dp),
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { }
.background(JetsnackTheme.colors.uiFloated)
.padding(horizontal = 24.dp, vertical = 16.dp)
.skipToLookaheadSize(),
) {
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
IconButton(onClick = onDismiss) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(Res.string.close)
)
}
Text(
text = stringResource(Res.string.label_filters),
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(top = 8.dp, end = 48.dp),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge
)
val resetEnabled = sortState != defaultFilter
IconButton(
onClick = { /* TODO: Open search */ },
enabled = resetEnabled
) {
val fontWeight = if (resetEnabled) {
FontWeight.Bold
} else {
FontWeight.Normal
}
Text(
text = stringResource(Res.string.reset),
style = MaterialTheme.typography.bodyMedium,
fontWeight = fontWeight,
color = JetsnackTheme.colors.uiBackground
.copy(alpha = if (!resetEnabled) 0.38f else 1f)
)
}
}
SortFiltersSection(
sortState = sortState,
onFilterChange = { filter ->
@ -125,11 +154,11 @@ fun FilterScreen(
}
)
FilterChipSection(
title = stringResource(id = MppR.string.price),
title = stringResource(Res.string.price),
filters = priceFilters
)
FilterChipSection(
title = stringResource(id = MppR.string.category),
title = stringResource(Res.string.category),
filters = categoryFilters
)
@ -140,7 +169,7 @@ fun FilterScreen(
}
)
FilterChipSection(
title = stringResource(id = MppR.string.lifestyle),
title = stringResource(Res.string.lifestyle),
filters = lifeStyleFilters
)
}
@ -148,15 +177,11 @@ fun FilterScreen(
}
}
@Composable
expect fun SnackDialog(onCloseRequest: () -> Unit, content: @Composable () -> Unit)
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun FilterChipSection(title: String, filters: List<Filter>) {
FilterTitle(text = title)
FlowRow(
mainAxisAlignment = FlowMainAxisAlignment.Center,
androidx.compose.foundation.layout.FlowRow(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp, bottom = 16.dp)
@ -173,7 +198,7 @@ fun FilterChipSection(title: String, filters: List<Filter>) {
@Composable
fun SortFiltersSection(sortState: String, onFilterChange: (Filter) -> Unit) {
FilterTitle(text = stringResource(id = MppR.string.sort))
FilterTitle(text = stringResource(Res.string.sort))
Column(Modifier.padding(bottom = 24.dp)) {
SortFilters(
sortState = sortState,
@ -201,13 +226,14 @@ fun SortFilters(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun MaxCalories(sliderPosition: Float, onValueChanged: (Float) -> Unit) {
FlowRow {
FilterTitle(text = stringResource(id = MppR.string.max_calories))
androidx.compose.foundation.layout.FlowRow {
FilterTitle(text = stringResource(Res.string.max_calories))
Text(
text = stringResource(id = MppR.string.per_serving),
style = MaterialTheme.typography.body2,
text = stringResource(Res.string.per_serving),
style = MaterialTheme.typography.bodyMedium,
color = JetsnackTheme.colors.brand,
modifier = Modifier.padding(top = 5.dp, start = 10.dp)
)
@ -223,7 +249,8 @@ fun MaxCalories(sliderPosition: Float, onValueChanged: (Float) -> Unit) {
.fillMaxWidth(),
colors = SliderDefaults.colors(
thumbColor = JetsnackTheme.colors.brand,
activeTrackColor = JetsnackTheme.colors.brand
activeTrackColor = JetsnackTheme.colors.brand,
inactiveTrackColor = JetsnackTheme.colors.iconInteractive
)
)
}
@ -232,11 +259,12 @@ fun MaxCalories(sliderPosition: Float, onValueChanged: (Float) -> Unit) {
fun FilterTitle(text: String) {
Text(
text = text,
style = MaterialTheme.typography.h6,
style = MaterialTheme.typography.titleLarge,
color = JetsnackTheme.colors.brand,
modifier = Modifier.padding(bottom = 8.dp)
)
}
@Composable
fun SortOption(
text: String,
@ -254,7 +282,7 @@ fun SortOption(
}
Text(
text = text,
style = MaterialTheme.typography.subtitle1,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.padding(start = 10.dp)
.weight(1f)
@ -267,9 +295,4 @@ fun SortOption(
)
}
}
}
//@Preview
@Composable
fun FilterScreenPreview() {
FilterScreen(onDismiss = {})
}
}

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

@ -16,31 +16,38 @@
package com.example.jetsnack.ui.home
import androidx.annotation.FloatRange
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
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.animation.fadeIn
import androidx.compose.animation.fadeOut
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.navigationBarsPadding
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.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@ -58,39 +65,109 @@ 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 androidx.compose.ui.util.lerp
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDeepLink
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.example.common.generated.resources.*
import com.example.common.generated.resources.Res
import com.example.common.generated.resources.home_cart
import com.example.common.generated.resources.home_feed
import com.example.common.generated.resources.home_search
import com.example.jetsnack.ui.LocalNavAnimatedVisibilityScope
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.home.cart.Cart
import com.example.jetsnack.ui.home.search.Search
import com.example.jetsnack.ui.snackdetail.nonSpatialExpressiveSpring
import com.example.jetsnack.ui.snackdetail.spatialExpressiveSpring
import com.example.jetsnack.ui.theme.JetsnackTheme
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import kotlin.jvm.JvmSuppressWildcards
//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)
// }
//}
fun NavGraphBuilder.composableWithCompositionLocal(
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
enterTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
)? = {
fadeIn(nonSpatialExpressiveSpring())
},
exitTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
)? = {
fadeOut(nonSpatialExpressiveSpring())
},
popEnterTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
)? =
enterTransition,
popExitTransition: (
@JvmSuppressWildcards
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
)? =
exitTransition,
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
) {
composable(
route,
arguments,
deepLinks,
enterTransition,
exitTransition,
popEnterTransition,
popExitTransition
) {
CompositionLocalProvider(
LocalNavAnimatedVisibilityScope provides this@composable
) {
content(it)
}
}
}
fun NavGraphBuilder.addHomeGraph(
onSnackSelected: (Long, String, NavBackStackEntry) -> Unit,
modifier: Modifier = Modifier
) {
composable(HomeSections.FEED.route) { from ->
Feed(
onSnackClick = { id, origin -> onSnackSelected(id, origin, from) },
modifier
)
}
composable(HomeSections.SEARCH.route) { from ->
Search(
onSnackClick = { id, origin -> onSnackSelected(id, origin, from) },
modifier
)
}
composable(HomeSections.CART.route) { from ->
Cart(
onSnackClick = { id, origin -> onSnackSelected(id, origin, from) },
modifier
)
}
composable(HomeSections.PROFILE.route) {
Profile(modifier)
}
}
enum class HomeSections(
val title: Int, // @StringRes
val title: StringResource,
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")
FEED(Res.string.home_feed, Icons.Outlined.Home, "home/feed"),
SEARCH(Res.string.home_search, Icons.Outlined.Search, "home/search"),
CART(Res.string.home_cart, Icons.Outlined.ShoppingCart, "home/cart"),
PROFILE(Res.string.home_profile, Icons.Outlined.AccountCircle, "home/profile")
}
@Composable
@ -98,6 +175,7 @@ fun JetsnackBottomBar(
tabs: Array<HomeSections>,
currentRoute: String,
navigateToRoute: (String) -> Unit,
modifier: Modifier = Modifier,
color: Color = JetsnackTheme.colors.iconPrimary,
contentColor: Color = JetsnackTheme.colors.iconInteractive
) {
@ -105,25 +183,18 @@ fun JetsnackBottomBar(
val currentSection = tabs.first { it.route == currentRoute }
JetsnackSurface(
modifier = modifier,
color = color,
contentColor = contentColor
) {
val springSpec = SpringSpec<Float>(
// Determined experimentally
stiffness = 800f,
dampingRatio = 0.8f
)
val springSpec = spatialExpressiveSpring<Float>()
JetsnackBottomNavLayout(
selectedIndex = currentSection.ordinal,
itemCount = routes.size,
indicator = { JetsnackBottomNavIndicator() },
animSpec = springSpec,
modifier = Modifier.jetSnackNavigationBarsPadding()
modifier = Modifier.navigationBarsPadding()
) {
// 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
@ -132,11 +203,10 @@ fun JetsnackBottomBar(
JetsnackTheme.colors.iconInteractive
} else {
JetsnackTheme.colors.iconInteractiveInactive
}
},
label = "tint"
)
// TODO: implement uppercase using currentLocale
// val text = stringResource(section.title).uppercase(currentLocale)
val text = stringResource(section.title).uppercase()
JetsnackBottomNavigationItem(
@ -151,7 +221,7 @@ fun JetsnackBottomBar(
Text(
text = text,
color = tint,
style = MaterialTheme.typography.button,
style = MaterialTheme.typography.labelLarge,
maxLines = 1
)
},
@ -253,7 +323,10 @@ fun JetsnackBottomNavigationItem(
modifier: Modifier = Modifier
) {
// Animate the icon/text positions within the item based on selection
val animationProgress by animateFloatAsState(if (selected) 1f else 0f, animSpec)
val animationProgress by animateFloatAsState(
if (selected) 1f else 0f, animSpec,
label = "animation progress"
)
JetsnackBottomNavItemLayout(
icon = icon,
text = text,
@ -268,7 +341,7 @@ fun JetsnackBottomNavigationItem(
private fun JetsnackBottomNavItemLayout(
icon: @Composable BoxScope.() -> Unit,
text: @Composable BoxScope.() -> Unit,
animationProgress: Float, // @FloatRange(from = 0.0, to = 1.0)
@FloatRange(from = 0.0, to = 1.0) animationProgress: Float,
modifier: Modifier = Modifier
) {
Layout(
@ -313,7 +386,7 @@ private fun MeasureScope.placeTextAndIcon(
iconPlaceable: Placeable,
width: Int,
height: Int,
animationProgress: Float // @FloatRange(from = 0.0, to = 1.0)
@FloatRange(from = 0.0, to = 1.0) animationProgress: Float
): MeasureResult {
val iconY = (height - iconPlaceable.height) / 2
val textY = (height - textPlaceable.height) / 2
@ -349,15 +422,3 @@ 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 = { }
)
}
}

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

@ -16,27 +16,26 @@
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.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.example.jetsnack.*
import com.example.jetsnack.ui.theme.JetsnackTheme
import com.example.common.generated.resources.Res
import com.example.common.generated.resources.empty_state_search
import com.example.common.generated.resources.grab_beverage
import com.example.common.generated.resources.work_in_progress
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@Composable
fun Profile(modifier: Modifier = Modifier) {
fun Profile(
modifier: Modifier = Modifier
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
@ -45,30 +44,22 @@ fun Profile(modifier: Modifier = Modifier) {
.padding(24.dp)
) {
Image(
painterResource(MppR.drawable.empty_state_search),
painterResource(Res.drawable.empty_state_search),
contentDescription = null
)
Spacer(Modifier.height(24.dp))
Text(
text = stringResource(MppR.string.work_in_progress),
style = MaterialTheme.typography.subtitle1,
text = stringResource(Res.string.work_in_progress),
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(16.dp))
Text(
text = stringResource(MppR.string.grab_beverage),
style = MaterialTheme.typography.body2,
text = stringResource(Res.string.grab_beverage),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}
//@Preview
@Composable
fun ProfilePreview() {
JetsnackTheme {
Profile()
}
}

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

@ -1,20 +1,19 @@
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.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.padding
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.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.DeleteForever
import androidx.compose.material3.*
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
@ -25,28 +24,36 @@ 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.IntOffset
import androidx.compose.ui.unit.dp
import com.example.jetsnack.*
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.common.generated.resources.*
import com.example.common.generated.resources.Res
import com.example.common.generated.resources.cart_checkout
import com.example.common.generated.resources.label_remove
import com.example.common.generated.resources.remove_item
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.components.*
import com.example.jetsnack.ui.home.DestinationBar
import com.example.jetsnack.ui.snackdetail.nonSpatialExpressiveSpring
import com.example.jetsnack.ui.snackdetail.spatialExpressiveSpring
import com.example.jetsnack.ui.theme.AlphaNearOpaque
import com.example.jetsnack.ui.theme.JetsnackTheme
import com.example.jetsnack.ui.utils.formatPrice
import org.jetbrains.compose.resources.pluralStringResource
import org.jetbrains.compose.resources.stringResource
import kotlin.math.roundToInt
@Composable
fun Cart(
onSnackClick: (Long) -> Unit,
onSnackClick: (Long, String) -> Unit,
modifier: Modifier = Modifier,
viewModel: CartViewModel = provideCartViewModel()
viewModel: CartViewModel = viewModel(factory = CartViewModel.provideFactory())
) {
val orderLines by viewModel.collectOrderLinesAsState(viewModel.orderLines)
val orderLines by viewModel.orderLines.collectAsStateWithLifecycle()
val inspiredByCart = remember { SnackRepo.getInspiredByCart() }
Cart(
orderLines = orderLines,
@ -59,23 +66,6 @@ fun Cart(
)
}
@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>,
@ -83,11 +73,11 @@ fun Cart(
increaseItemCount: (Long) -> Unit,
decreaseItemCount: (Long) -> Unit,
inspiredByCart: SnackCollection,
onSnackClick: (Long) -> Unit,
onSnackClick: (Long, String) -> Unit,
modifier: Modifier = Modifier
) {
JetsnackSurface(modifier = modifier.fillMaxSize()) {
Box {
Box(modifier = Modifier.fillMaxSize()) {
CartContent(
orderLines = orderLines,
removeSnack = removeSnack,
@ -103,13 +93,6 @@ fun Cart(
}
}
@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>,
@ -117,18 +100,24 @@ private fun CartContent(
increaseItemCount: (Long) -> Unit,
decreaseItemCount: (Long) -> Unit,
inspiredByCart: SnackCollection,
onSnackClick: (Long) -> Unit,
onSnackClick: (Long, String) -> Unit,
modifier: Modifier = Modifier
) {
val snackCountFormattedString = rememberQuantityString(
MppR.plurals.cart_order_count, orderLines.size, orderLines.size
val snackCountFormattedString = pluralStringResource(
Res.plurals.cart_order_count, orderLines.size, orderLines.size
)
val itemAnimationSpecFade = nonSpatialExpressiveSpring<Float>()
val itemPlacementSpec = spatialExpressiveSpring<IntOffset>()
LazyColumn(modifier) {
item {
Spacer(Modifier.windowInsetsTopHeight(getCartContentInsets()))
item(key = "title") {
Spacer(
Modifier.windowInsetsTopHeight(
WindowInsets.statusBars.add(WindowInsets(top = 56.dp))
)
)
Text(
text = stringResource(MppR.string.cart_order_header, snackCountFormattedString),
style = MaterialTheme.typography.h6,
text = stringResource(Res.string.cart_order_header, snackCountFormattedString),
style = MaterialTheme.typography.titleLarge,
color = JetsnackTheme.colors.brand,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -138,87 +127,18 @@ private fun CartContent(
.wrapContentHeight()
)
}
items(orderLines) { orderLine ->
items(orderLines, key = { it.snack.id }) { 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
)
)
}
}
}
}
}
modifier = Modifier.animateItem(
fadeInSpec = itemAnimationSpecFade,
fadeOutSpec = itemAnimationSpecFade,
placementSpec = itemPlacementSpec
),
background = { progress ->
SwipeDismissItemBackground(progress)
},
) {
ActualCartItem(
CartItem(
orderLine = orderLine,
removeSnack = removeSnack,
increaseItemCount = increaseItemCount,
@ -227,14 +147,24 @@ private fun CartContent(
)
}
}
item {
item("summary") {
SummaryItem(
subtotal = orderLines.map { it.snack.price * it.count }.sum(),
modifier = Modifier.animateItem(
fadeInSpec = itemAnimationSpecFade,
fadeOutSpec = itemAnimationSpecFade,
placementSpec = itemPlacementSpec
),
subtotal = orderLines.sumOf { it.snack.price * it.count },
shippingCosts = 369
)
}
item {
item(key = "inspiredByCart") {
SnackCollection(
modifier = Modifier.animateItem(
fadeInSpec = itemAnimationSpecFade,
fadeOutSpec = itemAnimationSpecFade,
placementSpec = itemPlacementSpec
),
snackCollection = inspiredByCart,
onSnackClick = onSnackClick,
highlight = false
@ -244,6 +174,163 @@ private fun CartContent(
}
}
@Composable
private fun SwipeDismissItemBackground(progress: Float) {
Column(
modifier = Modifier
.background(JetsnackTheme.colors.uiBackground)
.fillMaxWidth()
.fillMaxHeight(),
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.Center
) {
// Set 4.dp padding only if progress is less than halfway
val padding: Dp by animateDpAsState(
if (progress < 0.5f) 4.dp else 0.dp, label = "padding"
)
BoxWithConstraints(
Modifier
.fillMaxWidth(progress)
) {
Surface(
modifier = Modifier
.padding(padding)
.fillMaxWidth()
.height(maxWidth)
.align(Alignment.Center),
shape = RoundedCornerShape(percent = ((1 - progress) * 100).roundToInt()),
color = JetsnackTheme.colors.error
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
// Icon must be visible while in this width range
if (progress in 0.125f..0.475f) {
// Icon alpha decreases as it is about to disappear
val iconAlpha: Float by animateFloatAsState(
if (progress > 0.4f) 0.5f else 1f, label = "icon alpha"
)
Icon(
imageVector = Icons.Filled.DeleteForever,
modifier = Modifier
.size(32.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 (progress > 0.5f) 1f else 0.5f, label = "text alpha"
)
if (progress > 0.5f) {
Text(
text = stringResource(Res.string.remove_item),
style = MaterialTheme.typography.titleMedium,
color = JetsnackTheme.colors.uiBackground,
textAlign = TextAlign.Center,
modifier = Modifier
.graphicsLayer(
alpha = textAlpha
)
)
}
}
}
}
}
}
@Composable
fun CartItem(
orderLine: OrderLine,
removeSnack: (Long) -> Unit,
increaseItemCount: (Long) -> Unit,
decreaseItemCount: (Long) -> Unit,
onSnackClick: (Long, String) -> Unit,
modifier: Modifier = Modifier
) {
val snack = orderLine.snack
Column(
modifier = modifier
.fillMaxWidth()
.clickable { onSnackClick(snack.id, "cart") }
.background(JetsnackTheme.colors.uiBackground)
.padding(horizontal = 24.dp)
) {
// Main content container
Box(
modifier = Modifier.fillMaxWidth()
) {
// Row containing image and text content
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
// Snack Image
SnackImage(
image = snack.image,
contentDescription = null,
modifier = Modifier.size(100.dp)
)
Spacer(modifier = Modifier.width(16.dp))
// Text content and Quantity Selector
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = snack.name,
style = MaterialTheme.typography.titleMedium,
color = JetsnackTheme.colors.textSecondary
)
Text(
text = snack.tagline,
style = MaterialTheme.typography.bodyLarge,
color = JetsnackTheme.colors.textHelp
)
Spacer(modifier = Modifier.height(8.dp))
// Row for price and quantity selector
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = formatPrice(snack.price),
style = MaterialTheme.typography.titleMedium,
color = JetsnackTheme.colors.textPrimary
)
Spacer(modifier = Modifier.weight(1f))
QuantitySelector(
count = orderLine.count,
decreaseItemCount = { decreaseItemCount(snack.id) },
increaseItemCount = { increaseItemCount(snack.id) }
)
}
}
}
// Remove Button positioned at the top-right corner
IconButton(
onClick = { removeSnack(snack.id) },
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 12.dp)
) {
Icon(
imageVector = Icons.Filled.Close,
tint = JetsnackTheme.colors.iconSecondary,
contentDescription = stringResource(Res.string.label_remove)
)
}
}
// Divider at the bottom
JetsnackDivider()
}
}
@Composable
fun SummaryItem(
subtotal: Long,
@ -252,8 +339,8 @@ fun SummaryItem(
) {
Column(modifier) {
Text(
text = stringResource(MppR.string.cart_summary_header),
style = MaterialTheme.typography.h6,
text = stringResource(Res.string.cart_summary_header),
style = MaterialTheme.typography.titleLarge,
color = JetsnackTheme.colors.brand,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -264,8 +351,8 @@ fun SummaryItem(
)
Row(modifier = Modifier.padding(horizontal = 24.dp)) {
Text(
text = stringResource(MppR.string.cart_subtotal_label),
style = MaterialTheme.typography.body1,
text = stringResource(Res.string.cart_subtotal_label),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
.weight(1f)
.wrapContentWidth(Alignment.Start)
@ -273,14 +360,14 @@ fun SummaryItem(
)
Text(
text = formatPrice(subtotal),
style = MaterialTheme.typography.body1,
style = MaterialTheme.typography.bodyLarge,
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,
text = stringResource(Res.string.cart_shipping_label),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
.weight(1f)
.wrapContentWidth(Alignment.Start)
@ -288,7 +375,7 @@ fun SummaryItem(
)
Text(
text = formatPrice(shippingCosts),
style = MaterialTheme.typography.body1,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.alignBy(LastBaseline)
)
}
@ -296,8 +383,8 @@ fun SummaryItem(
JetsnackDivider()
Row(modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) {
Text(
text = stringResource(MppR.string.cart_total_label),
style = MaterialTheme.typography.body1,
text = stringResource(Res.string.cart_total_label),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
.weight(1f)
.padding(end = 16.dp)
@ -306,7 +393,7 @@ fun SummaryItem(
)
Text(
text = formatPrice(subtotal + shippingCosts),
style = MaterialTheme.typography.subtitle1,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.alignBy(LastBaseline)
)
}
@ -321,6 +408,7 @@ private fun CheckoutBar(modifier: Modifier = Modifier) {
JetsnackTheme.colors.uiBackground.copy(alpha = AlphaNearOpaque)
)
) {
JetsnackDivider()
Row {
Spacer(Modifier.weight(1f))
@ -332,7 +420,7 @@ private fun CheckoutBar(modifier: Modifier = Modifier) {
.weight(1f)
) {
Text(
text = stringResource(id = MppR.string.cart_checkout),
text = stringResource(Res.string.cart_checkout),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Left,
maxLines = 1

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

@ -16,14 +16,18 @@
package com.example.jetsnack.ui.home.cart
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import com.example.jetsnack.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.example.common.generated.resources.Res
import com.example.common.generated.resources.cart_decrease_error
import com.example.common.generated.resources.cart_increase_error
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
import kotlin.reflect.KClass
/**
* Holds the contents of the cart and allows changes to it.
@ -33,7 +37,7 @@ import kotlinx.coroutines.flow.StateFlow
class CartViewModel(
private val snackbarManager: SnackbarManager,
snackRepository: SnackRepo
) : JetSnackCartViewModel() {
) : ViewModel() {
private val _orderLines: MutableStateFlow<List<OrderLine>> =
MutableStateFlow(snackRepository.getCart())
@ -48,7 +52,7 @@ class CartViewModel(
val currentCount = _orderLines.value.first { it.snack.id == snackId }.count
updateSnackCount(snackId, currentCount + 1)
} else {
snackbarManager.showMessage(MppR.string.cart_increase_error)
snackbarManager.showMessage(Res.string.cart_increase_error)
}
}
@ -63,7 +67,7 @@ class CartViewModel(
updateSnackCount(snackId, currentCount - 1)
}
} else {
snackbarManager.showMessage(MppR.string.cart_decrease_error)
snackbarManager.showMessage(Res.string.cart_decrease_error)
}
}
@ -81,12 +85,18 @@ class CartViewModel(
}
}
companion object // necessary for android (see `provideFactory` method)
}
expect abstract class JetSnackCartViewModel() {
@Composable
fun collectOrderLinesAsState(flow: StateFlow<List<OrderLine>>): State<List<OrderLine>>
/**
* Factory for CartViewModel that takes SnackbarManager as a dependency
*/
companion object {
fun provideFactory(
snackbarManager: SnackbarManager = SnackbarManager,
snackRepository: SnackRepo = SnackRepo
): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: KClass<T>, extras: CreationExtras): T {
return CartViewModel(snackbarManager, snackRepository) as T
}
}
}
}

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

@ -19,37 +19,31 @@ 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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.rememberSwipeToDismissBoxState
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)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
/**
* Holds the Swipe to dismiss composable, its animation and the current state
*/
/**
* 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,
background: @Composable (progress: Float) -> Unit,
content: @Composable (isDismissed: Boolean) -> Unit,
) {
// Hold the current state from the Swipe to Dismiss composable
val dismissState = rememberDismissState()
val dismissState = rememberSwipeToDismissBoxState()
// 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() }
val isDismissed = dismissState.currentValue == SwipeToDismissBoxValue.EndToStart
AnimatedVisibility(
modifier = modifier,
@ -57,12 +51,12 @@ fun SwipeDismissItem(
enter = enter,
exit = exit
) {
SwipeToDismiss(
SwipeToDismissBox(
modifier = modifier,
state = dismissState,
directions = directions,
background = { background(offset) },
dismissContent = { content(isDismissed) }
enableDismissFromStartToEnd = false,
backgroundContent = { background(dismissState.progress) },
content = { content(isDismissed) }
)
}
}
}

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

@ -30,8 +30,8 @@ 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.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -69,7 +69,7 @@ private fun SearchCategoryCollection(
Column(modifier) {
Text(
text = collection.name,
style = MaterialTheme.typography.h6,
style = MaterialTheme.typography.titleLarge,
color = JetsnackTheme.colors.textPrimary,
modifier = Modifier
.heightIn(min = 56.dp)
@ -113,14 +113,14 @@ private fun SearchCategory(
content = {
Text(
text = category.name,
style = MaterialTheme.typography.subtitle1,
// style = MaterialTheme.typography.subtitle1,
color = JetsnackTheme.colors.textSecondary,
modifier = Modifier
.padding(4.dp)
.padding(start = 8.dp)
)
SnackImage(
imageUrl = category.imageUrl,
image = category.image,
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
@ -150,17 +150,3 @@ private fun SearchCategory(
}
}
}
//@Preview
@Composable
private fun SearchCategoryPreview() {
JetsnackTheme {
SearchCategory(
category = SearchCategory(
name = "Desserts",
imageUrl = ""
),
gradient = JetsnackTheme.colors.gradient3_2
)
}
}

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

@ -16,43 +16,41 @@
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.clickable
import androidx.compose.foundation.layout.*
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.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.example.jetsnack.*
import com.example.jetsnack.model.Filter
import com.example.common.generated.resources.*
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.components.JetsnackButton
import com.example.jetsnack.ui.components.JetsnackDivider
import com.example.jetsnack.ui.components.SnackImage
import com.example.jetsnack.ui.theme.JetsnackTheme
import com.example.jetsnack.ui.utils.formatPrice
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@Composable
fun SearchResults(
searchResults: List<Snack>,
filters: List<Filter>,
onSnackClick: (Long) -> Unit
onSnackClick: (Long, String) -> Unit
) {
Column {
FilterBar(filters, onShowFilters = {})
Text(
text = stringResource(MppR.string.search_count, searchResults.size),
style = MaterialTheme.typography.h6,
text = stringResource(Res.string.search_count, searchResults.size),
style = MaterialTheme.typography.titleLarge,
color = JetsnackTheme.colors.textPrimary,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp)
)
@ -67,110 +65,73 @@ fun SearchResults(
@Composable
private fun SearchResult(
snack: Snack,
onSnackClick: (Long) -> Unit,
onSnackClick: (Long, String) -> 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)
// )
// }
// }
// Note the original example uses ConstraintLayout, but it's not available in ComposeMultiplatform
Column(
modifier = modifier
.fillMaxWidth()
.clickable { onSnackClick(snack.id, "search") }
.padding(horizontal = 24.dp)
) {
if (showDivider) {
JetsnackDivider()
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Snack Image
SnackImage(
image = snack.image,
contentDescription = null,
modifier = Modifier.size(100.dp)
)
Spacer(modifier = Modifier.width(16.dp))
// Text Content
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Center
) {
Text(
text = snack.name,
style = MaterialTheme.typography.titleMedium,
color = JetsnackTheme.colors.textSecondary
)
Text(
text = snack.tagline,
style = MaterialTheme.typography.bodyLarge,
color = JetsnackTheme.colors.textHelp
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = formatPrice(snack.price),
style = MaterialTheme.typography.titleMedium,
color = JetsnackTheme.colors.textPrimary
)
}
Spacer(modifier = Modifier.width(16.dp))
// Add Button
JetsnackButton(
onClick = { /* TODO: Implement add action */ },
shape = CircleShape,
contentPadding = PaddingValues(0.dp),
modifier = Modifier.size(36.dp)
) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription = stringResource(Res.string.label_add)
)
}
}
}
}
@Composable
fun NoResults(
query: String,
@ -184,36 +145,22 @@ fun NoResults(
.padding(24.dp)
) {
Image(
painterResource(MppR.drawable.empty_state_search),
painterResource(Res.drawable.empty_state_search),
contentDescription = null
)
Spacer(Modifier.height(24.dp))
Text(
text = stringResource(MppR.string.search_no_matches, query),
style = MaterialTheme.typography.subtitle1,
text = stringResource(Res.string.search_no_matches, query),
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(16.dp))
Text(
text = stringResource(MppR.string.search_no_matches_retry),
style = MaterialTheme.typography.body2,
text = stringResource(Res.string.search_no_matches_retry),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}
//@Preview
@Composable
private fun SearchResultPreview() {
JetsnackTheme {
JetsnackSurface {
SearchResult(
snack = snacks[0],
onSnackClick = { },
showDivider = false
)
}
}
}
}

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

@ -16,61 +16,37 @@
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.layout.*
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.automirrored.outlined.ArrowBack
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.material3.*
import androidx.compose.runtime.*
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.common.generated.resources.Res
import com.example.common.generated.resources.label_back
import com.example.common.generated.resources.label_search
import com.example.common.generated.resources.search_jetsnack
import com.example.jetsnack.model.*
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
import org.jetbrains.compose.resources.stringResource
@Composable
fun Search(
onSnackClick: (Long) -> Unit,
onSnackClick: (Long, String) -> Unit,
modifier: Modifier = Modifier,
state: SearchState = rememberSearchState()
) {
JetsnackSurface(modifier = modifier.fillMaxSize()) {
Column {
Spacer(modifier = Modifier.jetSnackStatusBarsPadding())
Spacer(modifier = Modifier.statusBarsPadding())
SearchBar(
query = state.query,
onQueryChange = { state.query = it },
@ -90,13 +66,16 @@ fun Search(
SearchDisplay.Categories -> SearchCategories(state.categories)
SearchDisplay.Suggestions -> SearchSuggestions(
suggestions = state.suggestions,
onSuggestionSelect = { suggestion -> state.query = TextFieldValue(suggestion) }
onSuggestionSelect = { suggestion ->
state.query = TextFieldValue(suggestion)
}
)
SearchDisplay.Results -> SearchResults(
state.searchResults,
state.filters,
onSnackClick
)
SearchDisplay.NoResults -> NoResults(state.query.text)
}
}
@ -188,9 +167,9 @@ private fun SearchBar(
if (searchFocused) {
IconButton(onClick = onClearQuery) {
Icon(
imageVector = mirroringBackIcon(),
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
tint = JetsnackTheme.colors.iconPrimary,
contentDescription = stringResource(MppR.string.label_back)
contentDescription = stringResource(Res.string.label_back)
)
}
}
@ -231,29 +210,12 @@ private fun SearchHint() {
Icon(
imageVector = Icons.Outlined.Search,
tint = JetsnackTheme.colors.textHelp,
contentDescription = stringResource(MppR.string.label_search)
contentDescription = stringResource(Res.string.label_search)
)
Spacer(Modifier.width(8.dp))
Text(
text = stringResource(MppR.string.search_jetsnack),
text = stringResource(Res.string.search_jetsnack),
color = JetsnackTheme.colors.textHelp
)
}
}
//@Preview
@Composable
private fun SearchBarPreview() {
JetsnackTheme {
JetsnackSurface {
SearchBar(
query = TextFieldValue(""),
onQueryChange = { },
searchFocused = false,
onSearchFocusChange = { },
onClearQuery = { },
searching = false
)
}
}
}
}

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

@ -16,25 +16,17 @@
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.layout.*
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.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.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
@ -68,7 +60,7 @@ private fun SuggestionHeader(
) {
Text(
text = name,
style = MaterialTheme.typography.h6,
style = MaterialTheme.typography.titleLarge,
color = JetsnackTheme.colors.textPrimary,
modifier = modifier
.heightIn(min = 56.dp)
@ -85,7 +77,7 @@ private fun Suggestion(
) {
Text(
text = suggestion,
style = MaterialTheme.typography.subtitle1,
style = MaterialTheme.typography.titleMedium,
modifier = modifier
.heightIn(min = 48.dp)
.clickable { onSuggestionSelect(suggestion) }
@ -93,16 +85,3 @@ private fun Suggestion(
.wrapContentSize(Alignment.CenterStart)
)
}
//@Preview
@Composable
fun PreviewSuggestions() {
JetsnackTheme {
JetsnackSurface {
SearchSuggestions(
suggestions = SearchRepo.getSuggestions(),
onSuggestionSelect = { }
)
}
}
}

107
examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/navigation/JetsnackNavController.kt

@ -0,0 +1,107 @@
/*
* 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.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.lifecycle.Lifecycle
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDestination
import androidx.navigation.NavGraph
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.example.jetsnack.ui.home.HomeSections
/**
* Destinations used in the [JetsnackApp].
*/
object MainDestinations {
const val HOME_ROUTE = "home"
const val SNACK_DETAIL_ROUTE = "snack"
const val SNACK_ID_KEY = "snackId"
const val ORIGIN = "origin"
}
/**
* Remembers and creates an instance of [JetsnackNavController]
*/
@Composable
fun rememberJetsnackNavController(
navController: NavHostController = rememberNavController()
): JetsnackNavController = remember(navController) {
JetsnackNavController(navController)
}
/**
* Responsible for holding UI Navigation logic.
*/
@Stable
class JetsnackNavController(
val navController: NavHostController,
) {
// ----------------------------------------------------------
// Navigation state source of truth
// ----------------------------------------------------------
fun upPress() {
navController.navigateUp()
}
fun navigateToBottomBarRoute(route: String) {
if (route != navController.currentDestination?.route) {
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).navigatorName) {
saveState = true
}
}
}
}
fun navigateToSnackDetail(snackId: Long, origin: String, from: NavBackStackEntry) {
// In order to discard duplicated navigation events, we check the Lifecycle
if (from.lifecycleIsResumed()) {
navController.navigate("${MainDestinations.SNACK_DETAIL_ROUTE}/$snackId?origin=$origin")
}
}
}
/**
* 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.lifecycle.currentState == Lifecycle.State.RESUMED
private val NavGraph.startDestination: NavDestination?
get() = findNode(HomeSections.FEED.route)
/**
* 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 graph.parent?.findStartDestination() ?: graph
}

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

@ -16,7 +16,24 @@
package com.example.jetsnack.ui.snackdetail
//import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.animation.BoundsTransform
import androidx.compose.animation.EnterExitState
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -29,28 +46,41 @@ 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.navigationBarsPadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.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.automirrored.outlined.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
@ -58,24 +88,27 @@ 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 androidx.compose.ui.util.lerp
import com.example.common.generated.resources.*
import com.example.common.generated.resources.Res
import com.example.common.generated.resources.label_back
import com.example.common.generated.resources.see_less
import com.example.common.generated.resources.see_more
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.LocalNavAnimatedVisibilityScope
import com.example.jetsnack.ui.LocalSharedTransitionScope
import com.example.jetsnack.ui.SnackSharedElementKey
import com.example.jetsnack.ui.SnackSharedElementType
import com.example.jetsnack.ui.components.*
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 org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.stringResource
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
@ -88,201 +121,366 @@ private val ExpandedImageSize = 300.dp
private val CollapsedImageSize = 150.dp
private val HzPadding = Modifier.padding(horizontal = 24.dp)
fun <T> spatialExpressiveSpring() = spring<T>(
dampingRatio = 0.8f,
stiffness = 380f
)
fun <T> nonSpatialExpressiveSpring() = spring<T>(
dampingRatio = 1f,
stiffness = 1600f
)
@OptIn(ExperimentalSharedTransitionApi::class)
val snackDetailBoundsTransform = BoundsTransform { _, _ ->
spatialExpressiveSpring()
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun SnackDetail(
snackId: Long,
upPress: () -> Unit,
onSnackClick: (Long) -> Unit,
origin: String,
upPress: () -> 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))
val sharedTransitionScope = LocalSharedTransitionScope.current
?: throw IllegalStateException("No Scope found")
val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current
?: throw IllegalStateException("No Scope found")
val roundedCornerAnim by animatedVisibilityScope.transition
.animateDp(label = "rounded corner") { enterExit: EnterExitState ->
when (enterExit) {
EnterExitState.PreEnter -> 20.dp
EnterExitState.Visible -> 0.dp
EnterExitState.PostExit -> 20.dp
}
}
with(sharedTransitionScope) {
Box(
Modifier
.clip(RoundedCornerShape(roundedCornerAnim))
.sharedBounds(
rememberSharedContentState(
key = SnackSharedElementKey(
snackId = snack.id,
origin = origin,
type = SnackSharedElementType.Bounds
)
),
animatedVisibilityScope,
clipInOverlayDuringTransition =
OverlayClip(RoundedCornerShape(roundedCornerAnim)),
boundsTransform = snackDetailBoundsTransform,
exit = fadeOut(nonSpatialExpressiveSpring()),
enter = fadeIn(nonSpatialExpressiveSpring()),
)
.fillMaxSize()
.background(color = JetsnackTheme.colors.uiBackground)
) {
val scroll = rememberScrollState(0)
Header(snack.id, origin = origin)
Body(related, scroll)
Title(snack, origin) { scroll.value }
Image(snackId, origin, snack.image) { scroll.value }
Up(upPress)
CartBottomBar(modifier = Modifier.align(Alignment.BottomCenter))
}
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun Header() {
Spacer(
modifier = Modifier
.height(280.dp)
.fillMaxWidth()
.background(Brush.horizontalGradient(JetsnackTheme.colors.tornado1))
)
private fun Header(snackId: Long, origin: String) {
val sharedTransitionScope = LocalSharedTransitionScope.current
?: throw IllegalArgumentException("No Scope found")
val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current
?: throw IllegalArgumentException("No Scope found")
with(sharedTransitionScope) {
val brushColors = JetsnackTheme.colors.tornado1
val infiniteTransition = rememberInfiniteTransition(label = "background")
val targetOffset = with(LocalDensity.current) {
1000.dp.toPx()
}
val offset by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = targetOffset,
animationSpec = infiniteRepeatable(
tween(50000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
label = "offset"
)
Spacer(
modifier = Modifier
.sharedBounds(
rememberSharedContentState(
key = SnackSharedElementKey(
snackId = snackId,
origin = origin,
type = SnackSharedElementType.Background
)
),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = snackDetailBoundsTransform,
enter = fadeIn(nonSpatialExpressiveSpring()),
exit = fadeOut(nonSpatialExpressiveSpring()),
resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds()
)
.height(280.dp)
.fillMaxWidth()
.blur(40.dp)
.drawWithCache {
val brushSize = 400f
val brush = Brush.linearGradient(
colors = brushColors,
start = Offset(offset, offset),
end = Offset(offset + brushSize, offset + brushSize),
tileMode = TileMode.Mirror
)
onDrawBehind {
drawRect(brush)
}
}
)
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@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
private fun SharedTransitionScope.Up(upPress: () -> Unit) {
val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current
?: throw IllegalArgumentException("No Scope found")
with(animatedVisibilityScope) {
IconButton(
onClick = upPress,
modifier = Modifier
.renderInSharedTransitionScopeOverlay(zIndexInOverlay = 3f)
.statusBarsPadding()
.padding(horizontal = 16.dp, vertical = 10.dp)
.size(36.dp)
.animateEnterExit(
enter = scaleIn(tween(300, delayMillis = 300)),
exit = scaleOut(tween(20))
)
.background(
color = Neutral8.copy(alpha = 0.32f),
shape = CircleShape
)
) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
tint = JetsnackTheme.colors.iconInteractive,
contentDescription = stringResource(Res.string.label_back),
)
) {
Icon(
imageVector = mirroringBackIcon(),
tint = JetsnackTheme.colors.iconInteractive,
contentDescription = stringResource(MppR.string.label_back)
)
}
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun Body(
related: List<SnackCollection>,
scroll: ScrollState,
onSnackClick: (Long) -> Unit,
scroll: ScrollState
) {
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
)
val sharedTransitionScope =
LocalSharedTransitionScope.current ?: throw IllegalStateException("No scope found")
with(sharedTransitionScope) {
Column(modifier = Modifier.skipToLookaheadSize()) {
Spacer(
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding()
.height(MinTitleOffset)
)
Spacer(Modifier.height(16.dp))
JetsnackDivider()
Column(
modifier = Modifier.verticalScroll(scroll)
) {
Spacer(Modifier.height(GradientScroll))
Spacer(Modifier.height(ImageOverlap))
JetsnackSurface(
Modifier
.fillMaxWidth()
.padding(top = 16.dp),
) {
Column {
Spacer(Modifier.height(TitleHeight))
Text(
text = stringResource(Res.string.detail_header),
style = MaterialTheme.typography.labelSmall,
color = JetsnackTheme.colors.textHelp,
modifier = HzPadding
)
Spacer(Modifier.height(16.dp))
var seeMore by remember { mutableStateOf(true) }
with(sharedTransitionScope) {
Text(
text = stringResource(Res.string.detail_placeholder),
style = MaterialTheme.typography.bodyLarge,
color = JetsnackTheme.colors.textHelp,
maxLines = if (seeMore) 5 else Int.MAX_VALUE,
overflow = TextOverflow.Ellipsis,
modifier = HzPadding.skipToLookaheadSize()
related.forEach { snackCollection ->
key(snackCollection.id) {
SnackCollection(
snackCollection = snackCollection,
onSnackClick = onSnackClick,
highlight = false
)
}
}
val textButton = if (seeMore) {
stringResource(Res.string.see_more)
} else {
stringResource(Res.string.see_less)
}
Spacer(
modifier = Modifier
.padding(bottom = BottomBarHeight)
.jetSnackNavigationBarsPadding()
.height(8.dp)
)
Text(
text = textButton,
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Center,
color = JetsnackTheme.colors.textLink,
modifier = Modifier
.heightIn(20.dp)
.fillMaxWidth()
.padding(top = 15.dp)
.clickable {
seeMore = !seeMore
}
.skipToLookaheadSize()
)
Spacer(Modifier.height(40.dp))
Text(
text = stringResource(Res.string.ingredients),
style = MaterialTheme.typography.labelSmall,
color = JetsnackTheme.colors.textHelp,
modifier = HzPadding
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(Res.string.ingredients_list),
style = MaterialTheme.typography.bodyLarge,
color = JetsnackTheme.colors.textHelp,
modifier = HzPadding
)
Spacer(Modifier.height(16.dp))
JetsnackDivider()
related.forEach { snackCollection ->
key(snackCollection.id) {
SnackCollection(
snackCollection = snackCollection,
onSnackClick = { _, _ -> },
index = 0,
highlight = false
)
}
}
Spacer(
modifier = Modifier
.padding(bottom = BottomBarHeight)
.navigationBarsPadding()
.height(8.dp)
)
}
}
}
}
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
private fun Title(snack: Snack, origin: String, scrollProvider: () -> Int) {
val maxOffset = with(LocalDensity.current) { MaxTitleOffset.toPx() }
val minOffset = with(LocalDensity.current) { MinTitleOffset.toPx() }
val sharedTransitionScope = LocalSharedTransitionScope.current
?: throw IllegalArgumentException("No Scope found")
val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current
?: throw IllegalArgumentException("No Scope found")
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())
with(sharedTransitionScope) {
Column(
verticalArrangement = Arrangement.Bottom,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = TitleHeight)
.statusBarsPadding()
.offset {
val scroll = scrollProvider()
val offset = (maxOffset - scroll).coerceAtLeast(minOffset)
IntOffset(x = 0, y = offset.toInt())
}
.background(JetsnackTheme.colors.uiBackground)
) {
Spacer(Modifier.height(16.dp))
Text(
text = snack.name,
fontStyle = FontStyle.Italic,
style = MaterialTheme.typography.headlineMedium,
color = JetsnackTheme.colors.textSecondary,
modifier = HzPadding
.sharedBounds(
rememberSharedContentState(
key = SnackSharedElementKey(
snackId = snack.id,
origin = origin,
type = SnackSharedElementType.Title
)
),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = snackDetailBoundsTransform
)
.wrapContentWidth()
)
Text(
text = snack.tagline,
fontStyle = FontStyle.Italic,
style = MaterialTheme.typography.titleSmall,
fontSize = 20.sp,
color = JetsnackTheme.colors.textHelp,
modifier = HzPadding
.sharedBounds(
rememberSharedContentState(
key = SnackSharedElementKey(
snackId = snack.id,
origin = origin,
type = SnackSharedElementType.Tagline
)
),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = snackDetailBoundsTransform
)
.wrapContentWidth()
)
Spacer(Modifier.height(4.dp))
with(animatedVisibilityScope) {
Text(
text = formatPrice(snack.price),
style = MaterialTheme.typography.titleLarge,
color = JetsnackTheme.colors.textPrimary,
modifier = HzPadding
.animateEnterExit(
enter = fadeIn() + slideInVertically { -it / 3 },
exit = fadeOut() + slideOutVertically { -it / 3 }
)
.skipToLookaheadSize()
)
}
.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()
Spacer(Modifier.height(8.dp))
JetsnackDivider(modifier = Modifier)
}
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun Image(
imageUrl: String,
snackId: Long,
origin: String,
imageRes: DrawableResource,
scrollProvider: () -> Int
) {
val collapseRange = with(LocalDensity.current) { (MaxTitleOffset - MinTitleOffset).toPx() }
@ -292,13 +490,35 @@ private fun Image(
CollapsingImageLayout(
collapseFractionProvider = collapseFractionProvider,
modifier = HzPadding.then(Modifier.jetSnackStatusBarsPadding())
modifier = HzPadding.statusBarsPadding()
) {
SnackImage(
imageUrl = imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
val sharedTransitionScope = LocalSharedTransitionScope.current
?: throw IllegalStateException("No sharedTransitionScope found")
val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current
?: throw IllegalStateException("No animatedVisibilityScope found")
with(sharedTransitionScope) {
SnackImage(
image = imageRes,
contentDescription = null,
modifier = Modifier
.sharedBounds(
rememberSharedContentState(
key = SnackSharedElementKey(
snackId = snackId,
origin = origin,
type = SnackSharedElementType.Image
)
),
animatedVisibilityScope = animatedVisibilityScope,
exit = fadeOut(),
enter = fadeIn(),
boundsTransform = snackDetailBoundsTransform
)
.fillMaxSize()
)
}
}
}
@ -336,74 +556,59 @@ private fun CollapsingImageLayout(
}
}
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()
}
@OptIn(ExperimentalSharedTransitionApi::class)
@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
val (count, updateCount) = remember { mutableIntStateOf(1) }
val sharedTransitionScope =
LocalSharedTransitionScope.current ?: throw IllegalStateException("No Shared scope")
val animatedVisibilityScope =
LocalNavAnimatedVisibilityScope.current ?: throw IllegalStateException("No Shared scope")
with(sharedTransitionScope) {
with(animatedVisibilityScope) {
JetsnackSurface(
modifier = modifier
.renderInSharedTransitionScopeOverlay(zIndexInOverlay = 4f)
.animateEnterExit(
enter = slideInVertically(
tween(
300,
delayMillis = 300
)
) { it } + fadeIn(tween(300, delayMillis = 300)),
exit = slideOutVertically(tween(50)) { it } +
fadeOut(tween(50))
)
) {
Column {
JetsnackDivider()
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.navigationBarsPadding()
.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(Res.string.add_to_cart),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
maxLines = 1
)
}
}
}
}
}
}
}
@Composable
expect fun Modifier.jetSnackNavigationBarsPadding(): Modifier
@Composable
expect fun Modifier.jetSnackStatusBarsPadding(): Modifier
@Composable
expect fun Modifier.jetSnackSystemBarsPadding(): Modifier
//@Preview
@Composable
private fun SnackDetailPreview() {
JetsnackTheme {
SnackDetail(
snackId = 1L,
upPress = { },
onSnackClick = { }
)
}
}
}

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

@ -17,7 +17,7 @@
package com.example.jetsnack.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(

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

@ -17,8 +17,7 @@
package com.example.jetsnack.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.Colors
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
@ -29,7 +28,6 @@ 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,
@ -100,7 +98,7 @@ fun JetsnackTheme(
ProvideJetsnackColors(colors) {
MaterialTheme(
colors = debugColors(darkTheme),
// colors = debugColors(darkTheme),
typography = Typography,
shapes = Shapes,
content = content
@ -290,21 +288,21 @@ private val LocalJetsnackColors = staticCompositionLocalOf<JetsnackColors> {
* 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
)
//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
//)

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

@ -16,7 +16,7 @@
package com.example.jetsnack.ui.theme
import androidx.compose.material.Typography
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
@ -27,92 +27,92 @@ 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
)
// 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
// )
)
}

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

@ -1,40 +0,0 @@
/*
* 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
)

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

@ -1,55 +0,0 @@
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 com.example.common.generated.resources.Res
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.ExperimentalResourceApi
import java.net.URL
import javax.imageio.ImageIO
private val imagesCache = mutableMapOf<String, ImageBitmap>()
@OptIn(ExperimentalAnimationApi::class, ExperimentalResourceApi::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 {
org.jetbrains.skia.Image.makeFromEncoded(Res.readBytes(imageUrl)).toComposeImageBitmap().also {
imagesCache[imageUrl] = it
img = it
}
} catch (e: Throwable) {
e.printStackTrace()
null
}
}
}
}
}

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

@ -1,59 +0,0 @@
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 com.example.common.generated.resources.Res
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned
import kotlinx.coroutines.*
import org.jetbrains.compose.resources.ExperimentalResourceApi
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, ExperimentalResourceApi::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 {
Image.makeFromEncoded(Res.readBytes(imageUrl)).toComposeImageBitmap().also {
imagesCache[imageUrl] = it
img = it
}
} catch (e: Throwable) {
e.printStackTrace()
null
}
}
}
}
}

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

@ -1,12 +0,0 @@
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

@ -1,15 +0,0 @@
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

@ -1,23 +0,0 @@
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

@ -1,84 +0,0 @@
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

@ -1,176 +0,0 @@
@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

@ -1,86 +0,0 @@
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()
}

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

@ -1,55 +0,0 @@
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
import kotlin.native.HiddenFromObjC
@Stable
@OptIn(kotlin.experimental.ExperimentalObjCRefinement::class)
@HiddenFromObjC // Remove after the bug is fixed: https://github.com/JetBrains/compose-multiplatform/issues/4848
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

@ -1,42 +0,0 @@
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

@ -1,9 +0,0 @@
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

@ -1,112 +0,0 @@
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) }
}

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

@ -1,18 +0,0 @@
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
import kotlin.native.HiddenFromObjC
@OptIn(kotlin.experimental.ExperimentalObjCRefinement::class)
@HiddenFromObjC // Remove after the bug is fixed: https://github.com/JetBrains/compose-multiplatform/issues/4848
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

@ -1,12 +0,0 @@
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))
}

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

@ -1,124 +0,0 @@
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

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

@ -1,21 +0,0 @@
package com.example.jetsnack.ui.snackdetail
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
actual fun Modifier.jetSnackNavigationBarsPadding(): Modifier =
this.windowInsetsPadding(WindowInsets.navigationBars)
@Composable
actual fun Modifier.jetSnackStatusBarsPadding(): Modifier =
this.windowInsetsPadding(WindowInsets.statusBars)
@Composable
actual fun Modifier.jetSnackSystemBarsPadding(): Modifier =
this.windowInsetsPadding(WindowInsets.systemBars)

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

@ -1,9 +1,12 @@
package com.example.jetsnack.ui.components
import kotlinx.browser.window
import kotlinx.coroutines.await
import org.jetbrains.skia.ExternalSymbolName
import org.jetbrains.skia.impl.NativePointer
import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.Int8Array
import org.w3c.fetch.Response
import org.w3c.xhr.XMLHttpRequest
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@ -14,21 +17,7 @@ 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("")
}
return window.fetch(url).await<Response>().arrayBuffer().await()
}
fun ArrayBuffer.toByteArray(): ByteArray {

54
examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt

@ -1,54 +0,0 @@
package com.example.jetsnack.ui.components
import androidx.compose.ui.Modifier
import androidx.compose.runtime.*
import androidx.compose.foundation.Image
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.layout.ContentScale
import com.example.common.generated.resources.Res
import kotlinx.coroutines.*
import com.example.jetsnack.model.snacks
import org.jetbrains.compose.resources.ExperimentalResourceApi
val imagesCache = mutableMapOf<String, ImageBitmap>()
@OptIn(ExperimentalResourceApi::class)
@Composable
actual fun SnackAsyncImage(
imageUrl: String,
contentDescription: String?,
modifier: Modifier
) {
var bitmap: ImageBitmap? by remember { mutableStateOf(null) }
if (bitmap != null) {
Image(bitmap!!, contentDescription = contentDescription, modifier = modifier, contentScale = ContentScale.Crop)
}
LaunchedEffect(imageUrl) {
if (imagesCache.contains(imageUrl)) {
bitmap = imagesCache[imageUrl]!!
} else {
imagesCache[imageUrl] = org.jetbrains.skia.Image.makeFromEncoded(
Res.readBytes(imageUrl)
).toComposeImageBitmap()
bitmap = imagesCache[imageUrl]
}
}
}
@OptIn(ExperimentalResourceApi::class)
suspend fun CoroutineScope.prepareImagesCache() {
val jobs = mutableListOf<Job>()
// We have not many images, so we can prepare and cache them upfront
snacks.forEach {
val j = launch {
imagesCache[it.imageUrl] = org.jetbrains.skia.Image.makeFromEncoded(
Res.readBytes(it.imageUrl)
).toComposeImageBitmap()
}
jobs.add(j)
}
joinAll(*jobs.toTypedArray())
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save