diff --git a/examples/jetsnack/.gitignore b/examples/jetsnack/.gitignore new file mode 100644 index 0000000000..88f2e2fcc6 --- /dev/null +++ b/examples/jetsnack/.gitignore @@ -0,0 +1,43 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store +/local.properties diff --git a/examples/jetsnack/README.md b/examples/jetsnack/README.md new file mode 100755 index 0000000000..872969974a --- /dev/null +++ b/examples/jetsnack/README.md @@ -0,0 +1,122 @@ +# Kotlin/Wasm Jetsnack example + +This example showcases a web version of the [Jetsnack application](https://github.com/android/compose-samples/tree/main/Jetsnack) built with [Compose Multiplatform for web](#compose-multiplatform-for-web) and [Kotlin/Wasm](#kotlinwasm). + +Check it out: + +[![Static Badge](https://img.shields.io/badge/online%20demo%20%F0%9F%9A%80-6b57ff?style=for-the-badge)](https://zal.im/wasm/jetsnack). + +![](screenshots/jetsnack.png) + +> **Note:** +> To learn more about the Jetsnack application, visit the [Jetsnack README.md](https://github.com/android/compose-samples/tree/main/Jetsnack). + +## Kotlin/Wasm + +> **Note:** +> Kotlin/Wasm is an [Alpha](https://kotlinlang.org/docs/components-stability.html) feature. It may be changed at any time. You can use it in scenarios before production. +> We would appreciate your feedback in [YouTrack](https://youtrack.jetbrains.com/issue/KT-56492). +> +> [Join the Kotlin/Wasm community](https://slack-chats.kotlinlang.org/c/webassembly). + +Kotlin/Wasm is a new target that enables you to compile Kotlin code to [WebAssembly (Wasm)](https://webassembly.org/). + +By compiling Kotlin code to WebAssembly, you can run it on any WebAssembly-compatible environment that meets Kotlin's requirements, including web browsers. + +With Kotlin/Wasm, you gain the capability to develop high-performance web applications and serverless functions, opening up a wide range of potential projects. + +## Compose Multiplatform for web + +> **Note:** +> Web support is an [Alpha](https://kotlinlang.org/docs/components-stability.html) feature. It may be changed at any time. +> You can use it in scenarios before production. +> We would appreciate your feedback in [GitHub](https://github.com/JetBrains/compose-multiplatform/issues). +> +> [Join the compose-web community](https://slack-chats.kotlinlang.org/c/compose-web). + +Compose Multiplatform for web enables sharing your mobile or desktop UIs on the web. + +Compose Multiplatform for web is based on [Kotlin/Wasm](https://kotl.in/wasm), the newest target for Kotlin Multiplatform projects. +This enables running your code in the browser, leveraging WebAssembly's advantages like high and consistent application performance. + +Follow the instructions in the sections below to try out this Jetsnack application built with Compose Multiplatform for web and Kotlin/Wasm. + +## Set up the environment + +Before starting, ensure you have the necessary IDE and browser setup to run the application. + +### IDE + +We recommend using [IntelliJ IDEA 2023.1 or later](https://www.jetbrains.com/idea/) to work with the project. +It supports Kotlin/Wasm out of the box. + +### Browser (for Kotlin/Wasm target) + +To run Kotlin/Wasm applications in a browser, you need a browser supporting the [Wasm Garbage Collection (GC) feature](https://github.com/WebAssembly/gc): + +**Chrome and Chromium-based** + +* **For version 119 or later:** + + Works by default. + +**Firefox** + +* **For version 120 or later:** + + Works by default. + +**Safari/WebKit** + +Wasm GC support is currently under +[active development](https://bugs.webkit.org/show_bug.cgi?id=247394). + +> **Note:** +> For more information about the browser versions, see the [Troubleshooting documentation](https://kotl.in/wasm_help/). + +## Build and run + +To build and run the Jetsnack application with Compose Multiplatform for web and Kotlin/Wasm: + +1. In IntelliJ IDEA, open the repository. +2. Navigate to the `compose-jetsnack` project folder. +3. Run the application by typing one of the following Gradle commands in the terminal: + +* **Web version:** + + `./gradlew :web:wasmJsRun` +
 
+ + Once the application starts, open the following URL in your browser: + + `http://localhost:8080` + + > **Note:** + > The port number can vary. If the port 8080 is unavailable, you can find the corresponding port number printed in the console + > after building the application. +
 
+ +* **Desktop version:** + + `./gradlew :desktop:run` +
 
+ +* **Android application:** + + `./gradlew :android:installDebug` +* +* **iOS application:** + +To setup the environment, please consult these [instructions](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-setup.html). + +## Feedback and questions + +Give it a try and share your feedback or questions in our [#compose-web](https://slack-chats.kotlinlang.org/c/compose-web) Slack channel. +[Get a Slack invite](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up). +You can also share your comments with [@bashorov](https://twitter.com/bashorov) on X (Twitter). + +## Learn more + +* [Compose Multiplatform](https://github.com/JetBrains/compose-multiplatform/#compose-multiplatform) +* [Kotlin/Wasm](https://kotl.in/wasm/) +* [Other Kotlin/Wasm examples](https://github.com/Kotlin/kotlin-wasm-examples/tree/main) diff --git a/examples/jetsnack/android/build.gradle.kts b/examples/jetsnack/android/build.gradle.kts new file mode 100644 index 0000000000..4df87ef167 --- /dev/null +++ b/examples/jetsnack/android/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + id("org.jetbrains.compose") + id("com.android.application") + kotlin("android") +} + +group "com.example" +version "1.0-SNAPSHOT" + +repositories { + jcenter() +} + +dependencies { + implementation(project(":common")) + implementation("androidx.activity:activity-compose:1.5.0") +} + +android { + compileSdk = 34 + namespace = "com.example.android" + defaultConfig { + applicationId = "com.example.android" + minSdk = 24 + targetSdk = 33 + versionCode = 1 + versionName = "1.0-SNAPSHOT" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } +} diff --git a/examples/jetsnack/android/src/main/AndroidManifest.xml b/examples/jetsnack/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..d456c02f9d --- /dev/null +++ b/examples/jetsnack/android/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/examples/jetsnack/android/src/main/java/com/example/android/MainActivity.kt b/examples/jetsnack/android/src/main/java/com/example/android/MainActivity.kt new file mode 100644 index 0000000000..faa1aee2c1 --- /dev/null +++ b/examples/jetsnack/android/src/main/java/com/example/android/MainActivity.kt @@ -0,0 +1,36 @@ +package com.example.android + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.material.MaterialTheme +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import com.example.jetsnack.R +import com.example.jetsnack.ui.JetsnackApp +import com.example.jetsnack.ui.theme.Karla +import com.example.jetsnack.ui.theme.Montserrat + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + Montserrat = FontFamily( + Font(R.font.montserrat_light, FontWeight.Light), + Font(R.font.montserrat_regular, FontWeight.Normal), + Font(R.font.montserrat_medium, FontWeight.Medium), + Font(R.font.montserrat_semibold, FontWeight.SemiBold) + ) + Karla = FontFamily( + Font(R.font.karla_regular, FontWeight.Normal), + Font(R.font.karla_bold, FontWeight.Bold) + ) + + setContent { + MaterialTheme { + JetsnackApp() + } + } + } +} \ No newline at end of file diff --git a/examples/jetsnack/build.gradle.kts b/examples/jetsnack/build.gradle.kts new file mode 100644 index 0000000000..e68882e7fe --- /dev/null +++ b/examples/jetsnack/build.gradle.kts @@ -0,0 +1,29 @@ +import org.jetbrains.compose.ComposeExtension + +group "com.example" +version "1.0-SNAPSHOT" + +allprojects { + repositories { + google() + mavenCentral() + } + + afterEvaluate { + extensions.findByType(ComposeExtension::class.java)?.apply { + val kotlinGeneration = project.property("kotlin.generation") + val composeCompilerVersion = project.property("compose.compiler.version.$kotlinGeneration") as String + kotlinCompilerPlugin.set(composeCompilerVersion) + val kotlinVersion = project.property("kotlin.version.$kotlinGeneration") as String + kotlinCompilerPluginArgs.add("suppressKotlinVersionCompatibilityCheck=$kotlinVersion") + } + } +} + +plugins { + kotlin("multiplatform") apply false + kotlin("android") apply false + id("com.android.application") apply false + id("com.android.library") apply false + id("org.jetbrains.compose") apply false +} diff --git a/examples/jetsnack/common/build.gradle.kts b/examples/jetsnack/common/build.gradle.kts new file mode 100644 index 0000000000..fd74614231 --- /dev/null +++ b/examples/jetsnack/common/build.gradle.kts @@ -0,0 +1,110 @@ +import org.jetbrains.compose.compose + +plugins { + kotlin("multiplatform") + id("org.jetbrains.compose") + id("com.android.library") +} + +group = "com.example" +version = "1.0-SNAPSHOT" + +kotlin { + androidTarget() + wasmJs { browser() } + + jvm("desktop") { + compilations.all { + kotlinOptions.jvmTarget = "17" + } + } + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "common" + isStatic = true + } + } + + applyDefaultHierarchyTemplate() + + sourceSets { + val commonMain by getting { + dependencies { + api(compose.runtime) + api(compose.foundation) + api(compose.material) + implementation(libs.kotlinx.coroutines) + } + } + val nonAndroidMain by creating { + dependsOn(commonMain) + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + val androidMain by getting { + dependencies { + api("androidx.appcompat:appcompat:1.5.1") + api("androidx.core:core-ktx:1.9.0") + implementation(libs.coil.kt.compose) + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.compose.ui.util) + implementation(libs.androidx.lifecycle.viewModelCompose) + implementation(libs.androidx.constraintlayout.compose) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.viewModelCompose) + implementation(libs.androidx.lifecycle.runtime.compose) + } + } + + val androidUnitTest by getting { + dependencies { + implementation("junit:junit:4.13.2") + } + } + val desktopMain by getting { + dependsOn(nonAndroidMain) + dependencies { + api(compose.preview) + } + } + val desktopTest by getting + + val wasmJsMain by getting { + dependencies { + implementation(kotlin("stdlib")) + } + dependsOn(nonAndroidMain) + } + + val iosMain by getting { + dependsOn(nonAndroidMain) + } + } +} + + +android { + compileSdk = 34 + namespace = "com.example.jetsnack" + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + defaultConfig { + minSdk = 24 + targetSdk = 33 + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } +} diff --git a/examples/jetsnack/common/src/androidMain/AndroidManifest.xml b/examples/jetsnack/common/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000000..4fb03756cc --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/drawableResources.android.kt b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/drawableResources.android.kt new file mode 100644 index 0000000000..f17000c779 --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/drawableResources.android.kt @@ -0,0 +1,13 @@ +package com.example.jetsnack + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.painter.Painter + +@Composable +actual fun painterResource(id: Int): Painter { + return androidx.compose.ui.res.painterResource(id) +} + +actual val MppR.drawable.empty_state_search: Int + get() = R.drawable.empty_state_search + diff --git a/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/model/createRandomUUID.kt b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/model/createRandomUUID.kt new file mode 100644 index 0000000000..cf4e30a02c --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/model/createRandomUUID.kt @@ -0,0 +1,7 @@ +package com.example.jetsnack.model + +import java.util.* + +actual fun createRandomUUID(): Long { + return UUID.randomUUID().mostSignificantBits +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/stringResource.android.kt b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/stringResource.android.kt new file mode 100644 index 0000000000..68b497ec9e --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/stringResource.android.kt @@ -0,0 +1,114 @@ +@file:Suppress("PrivatePropertyName") + +package com.example.jetsnack + +import androidx.compose.runtime.Composable + +@Composable +actual fun stringResource(id: Int): String { + return androidx.compose.ui.res.stringResource(id) +} + +@Composable +actual fun stringResource(id: Int, part: String): String { + return androidx.compose.ui.res.stringResource(id, part) +} + +@Composable +actual fun stringResource(id: Int, count: Int): String { + return androidx.compose.ui.res.stringResource(id, count) +} + + +// Filters +actual val MppR.string.label_filters: Int get() = R.string.label_filters + +// Qty +actual val MppR.string.quantity: Int get() = R.string.quantity + +actual val MppR.string.label_decrease: Int get() = R.string.label_decrease + +actual val MppR.string.label_increase: Int get() = R.string.label_increase + + +// Snack detail +actual val MppR.string.label_back: Int get() = R.string.label_back + +actual val MppR.string.detail_header: Int get() = R.string.detail_header + +actual val MppR.string.detail_placeholder: Int get() = R.string.detail_placeholder + +actual val MppR.string.see_more: Int get() = R.string.see_more + +actual val MppR.string.see_less: Int get() = R.string.see_less + +actual val MppR.string.ingredients: Int get() = R.string.ingredients + +actual val MppR.string.ingredients_list: Int get() = R.string.ingredients_list + +actual val MppR.string.add_to_cart: Int get() = R.string.add_to_cart + +// Home +actual val MppR.string.label_select_delivery: Int get() = R.string.label_select_delivery + + +// Filter +actual val MppR.string.max_calories: Int get() = R.string.max_calories + +actual val MppR.string.per_serving: Int get() = R.string.per_serving + +actual val MppR.string.sort: Int get() = R.string.sort + +actual val MppR.string.lifestyle: Int get() = R.string.lifestyle + +actual val MppR.string.category: Int get() = R.string.category + +actual val MppR.string.price: Int get() = R.string.price + +actual val MppR.string.reset: Int get() = R.string.reset + +actual val MppR.string.close: Int get() = R.string.close + +// Profile + +actual val MppR.string.work_in_progress: Int get() = R.string.work_in_progress + +actual val MppR.string.grab_beverage: Int get() = R.string.grab_beverage + +// Home +actual val MppR.string.home_feed: Int get() = R.string.home_feed + +actual val MppR.string.home_search: Int get() = R.string.home_search + +actual val MppR.string.home_cart: Int get() = R.string.home_cart + +actual val MppR.string.home_profile: Int get() = R.string.home_profile + + +// Search +actual val MppR.string.search_no_matches: Int get() = R.string.search_no_matches + +actual val MppR.string.search_no_matches_retry: Int get() = R.string.search_no_matches_retry + +actual val MppR.string.label_add: Int get() = R.string.label_add + +actual val MppR.string.search_count: Int get() = R.string.search_count + +actual val MppR.string.label_search: Int get() = R.string.label_search + +actual val MppR.string.search_jetsnack: Int get() = R.string.search_jetsnack + +actual val MppR.string.cart_increase_error: Int get() = R.string.cart_increase_error +actual val MppR.string.cart_decrease_error: Int get() = R.string.cart_decrease_error + + +// Cart +actual val MppR.plurals.cart_order_count: Int get() = R.plurals.cart_order_count +actual val MppR.string.cart_order_header: Int get() = R.string.cart_order_header +actual val MppR.string.remove_item: Int get() = R.string.remove_item +actual val MppR.string.cart_summary_header: Int get() = R.string.cart_summary_header +actual val MppR.string.cart_subtotal_label: Int get() = R.string.cart_subtotal_label +actual val MppR.string.cart_shipping_label: Int get() = R.string.cart_shipping_label +actual val MppR.string.cart_total_label: Int get() = R.string.cart_total_label +actual val MppR.string.cart_checkout: Int get() = R.string.cart_checkout +actual val MppR.string.label_remove: Int get() = R.string.label_remove \ No newline at end of file diff --git a/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/JetsnackScaffoldContent.kt b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/JetsnackScaffoldContent.kt new file mode 100644 index 0000000000..61281fa863 --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/JetsnackScaffoldContent.kt @@ -0,0 +1,74 @@ +package com.example.jetsnack.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.navigation +import androidx.navigation.navArgument +import com.example.jetsnack.ui.home.Feed +import com.example.jetsnack.ui.home.HomeSections +import com.example.jetsnack.ui.home.Profile +import com.example.jetsnack.ui.home.cart.Cart +import com.example.jetsnack.ui.home.search.Search +import com.example.jetsnack.ui.snackdetail.SnackDetail + +@Composable +actual fun JetsnackScaffoldContent( + innerPaddingModifier: PaddingValues, + appState: MppJetsnackAppState +) { + NavHost( + navController = appState.navController, + startDestination = MainDestinations.HOME_ROUTE, + modifier = Modifier.padding(innerPaddingModifier) + ) { + jetsnackNavGraph( + onSnackSelected = appState::navigateToSnackDetail, + upPress = appState::upPress + ) + } +} + +private fun NavGraphBuilder.jetsnackNavGraph( + onSnackSelected: (Long, NavBackStackEntry) -> Unit, + upPress: () -> Unit, +) { + navigation( + route = MainDestinations.HOME_ROUTE, + startDestination = HomeSections.FEED.route + ) { + addHomeGraph(onSnackSelected) + } + composable( + "${MainDestinations.SNACK_DETAIL_ROUTE}/{${MainDestinations.SNACK_ID_KEY}}", + arguments = listOf(navArgument(MainDestinations.SNACK_ID_KEY) { type = NavType.LongType }) + ) { backStackEntry -> + val arguments = requireNotNull(backStackEntry.arguments) + val snackId = arguments.getLong(MainDestinations.SNACK_ID_KEY) + SnackDetail(snackId, upPress, onSnackClick = { onSnackSelected(snackId, backStackEntry) }) + } +} + +fun NavGraphBuilder.addHomeGraph( + onSnackSelected: (Long, NavBackStackEntry) -> Unit, + modifier: Modifier = Modifier +) { + composable(HomeSections.FEED.route) { from -> + Feed(onSnackClick = { id -> onSnackSelected(id, from) }, modifier) + } + composable(HomeSections.SEARCH.route) { from -> + Search(onSnackClick = { id -> onSnackSelected(id, from) }, modifier) + } + composable(HomeSections.CART.route) { from -> + Cart(onSnackClick = { id -> onSnackSelected(id, from) }, modifier) + } + composable(HomeSections.PROFILE.route) { + Profile(modifier) + } +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/MppJetsnackAppState.kt b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/MppJetsnackAppState.kt new file mode 100644 index 0000000000..69f2edbf50 --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/MppJetsnackAppState.kt @@ -0,0 +1,123 @@ +package com.example.jetsnack.ui + +import android.content.res.Resources +import androidx.compose.material.ScaffoldState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.* +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.Lifecycle +import androidx.navigation.* +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.example.jetsnack.model.SnackbarManager +import com.example.jetsnack.ui.home.HomeSections +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Stable +actual class MppJetsnackAppState( + actual val scaffoldState: ScaffoldState, + actual val snackbarManager: SnackbarManager, + actual val coroutineScope: CoroutineScope, + val navController: NavHostController, + val resources: Resources +) { + + init { + coroutineScope.launch { + snackbarManager.messages.collect { currentMessages -> + if (currentMessages.isNotEmpty()) { + val message = currentMessages[0] + val text = resources.getText(message.message).toString() + + // Display the snackbar on the screen. `showSnackbar` is a function + // that suspends until the snackbar disappears from the screen + scaffoldState.snackbarHostState.showSnackbar(text) + // Once the snackbar is gone or dismissed, notify the SnackbarManager + snackbarManager.setMessageShown(message.id) + } + } + } + } + + private val bottomBarRoutes = bottomBarTabs.map { it.route } + + actual val bottomBarTabs: Array + get() = HomeSections.values() + actual val currentRoute: String? + get() = navController.currentDestination?.route + + + @Composable + actual fun shouldShowBottomBar(): Boolean { + return navController + .currentBackStackEntryAsState().value?.destination?.route in bottomBarRoutes + } + + actual fun navigateToBottomBarRoute(route: String) { + if (route != currentRoute) { + navController.navigate(route) { + launchSingleTop = true + restoreState = true + // Pop up backstack to the first destination and save state. This makes going back + // to the start destination when pressing back in any other bottom tab. + popUpTo(findStartDestination(navController.graph).id) { + saveState = true + } + } + } + } + + fun navigateToSnackDetail(snackId: Long, from: NavBackStackEntry) { + // In order to discard duplicated navigation events, we check the Lifecycle + if (from.lifecycleIsResumed()) { + navController.navigate("${MainDestinations.SNACK_DETAIL_ROUTE}/$snackId") + } + } + + fun upPress() { + navController.navigateUp() + } +} + +@Suppress("UsePropertyAccessSyntax") +private fun NavBackStackEntry.lifecycleIsResumed() = + this.getLifecycle().currentState == Lifecycle.State.RESUMED + +private val NavGraph.startDestination: NavDestination? + get() = findNode(startDestinationId) + +/** + * Copied from similar function in NavigationUI.kt + * + * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt + */ +private tailrec fun findStartDestination(graph: NavDestination): NavDestination { + return if (graph is NavGraph) findStartDestination(graph.startDestination!!) else graph +} + +@Composable +actual fun rememberMppJetsnackAppState(): MppJetsnackAppState { + val scaffoldState = rememberScaffoldState() + val navController = rememberNavController() + val resources = resources() + val snackbarManager = SnackbarManager + val coroutineScope = rememberCoroutineScope() + + return remember(scaffoldState, navController, snackbarManager, resources, coroutineScope) { + MppJetsnackAppState(scaffoldState, snackbarManager, coroutineScope, navController, resources) + } +} + +/** + * A composable function that returns the [Resources]. It will be recomposed when `Configuration` + * gets updated. + */ +@Composable +@ReadOnlyComposable +private fun resources(): Resources { + LocalConfiguration.current + return LocalContext.current.resources +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt new file mode 100644 index 0000000000..fe715cb652 --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt @@ -0,0 +1,25 @@ +package com.example.jetsnack.ui.components + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.example.jetsnack.R + +@Composable +actual fun SnackAsyncImage(imageUrl: String, contentDescription: String?, modifier: Modifier) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .crossfade(true) + .build(), + contentDescription = contentDescription, + placeholder = painterResource(R.drawable.placeholder), + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop, + ) +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/SnackDialog.kt b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/SnackDialog.kt new file mode 100644 index 0000000000..3d9cd7f2f9 --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/SnackDialog.kt @@ -0,0 +1,9 @@ +package com.example.jetsnack.ui.home + +import androidx.compose.runtime.Composable +import androidx.compose.ui.window.Dialog + +@Composable +actual fun SnackDialog(onCloseRequest: () -> Unit, content: @Composable () -> Unit) { + Dialog(onDismissRequest = onCloseRequest, content = content) +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.android.kt b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.android.kt new file mode 100644 index 0000000000..87035a37d5 --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.android.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.home.cart + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ChainStyle +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.jetsnack.R +import com.example.jetsnack.model.OrderLine +import com.example.jetsnack.model.SnackRepo +import com.example.jetsnack.ui.components.JetsnackDivider +import com.example.jetsnack.ui.components.QuantitySelector +import com.example.jetsnack.ui.components.SnackImage +import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.utils.formatPrice + +@Composable +actual fun provideCartViewModel(): CartViewModel { + return viewModel(factory = CartViewModel.provideFactory()) +} + +@Composable +actual fun ActualCartItem( + orderLine: OrderLine, + removeSnack: (Long) -> Unit, + increaseItemCount: (Long) -> Unit, + decreaseItemCount: (Long) -> Unit, + onSnackClick: (Long) -> Unit, + modifier: Modifier +) { + val snack = orderLine.snack + ConstraintLayout( + modifier = modifier + .fillMaxWidth() + .clickable { onSnackClick(snack.id) } + .background(JetsnackTheme.colors.uiBackground) + .padding(horizontal = 24.dp) + + ) { + val (divider, image, name, tag, priceSpacer, price, remove, quantity) = createRefs() + createVerticalChain(name, tag, priceSpacer, price, chainStyle = ChainStyle.Packed) + SnackImage( + imageUrl = snack.imageUrl, + contentDescription = null, + modifier = Modifier + .size(100.dp) + .constrainAs(image) { + top.linkTo(parent.top, margin = 16.dp) + bottom.linkTo(parent.bottom, margin = 16.dp) + start.linkTo(parent.start) + } + ) + Text( + text = snack.name, + style = MaterialTheme.typography.subtitle1, + color = JetsnackTheme.colors.textSecondary, + modifier = Modifier.constrainAs(name) { + linkTo( + start = image.end, + startMargin = 16.dp, + end = remove.start, + endMargin = 16.dp, + bias = 0f + ) + } + ) + IconButton( + onClick = { removeSnack(snack.id) }, + modifier = Modifier + .constrainAs(remove) { + top.linkTo(parent.top) + end.linkTo(parent.end) + } + .padding(top = 12.dp) + ) { + Icon( + imageVector = Icons.Filled.Close, + tint = JetsnackTheme.colors.iconSecondary, + contentDescription = stringResource(R.string.label_remove) + ) + } + Text( + text = snack.tagline, + style = MaterialTheme.typography.body1, + color = JetsnackTheme.colors.textHelp, + modifier = Modifier.constrainAs(tag) { + linkTo( + start = image.end, + startMargin = 16.dp, + end = parent.end, + endMargin = 16.dp, + bias = 0f + ) + } + ) + Spacer( + Modifier + .height(8.dp) + .constrainAs(priceSpacer) { + linkTo(top = tag.bottom, bottom = price.top) + } + ) + Text( + text = formatPrice(snack.price), + style = MaterialTheme.typography.subtitle1, + color = JetsnackTheme.colors.textPrimary, + modifier = Modifier.constrainAs(price) { + linkTo( + start = image.end, + end = quantity.start, + startMargin = 16.dp, + endMargin = 16.dp, + bias = 0f + ) + } + ) + QuantitySelector( + count = orderLine.count, + decreaseItemCount = { decreaseItemCount(snack.id) }, + increaseItemCount = { increaseItemCount(snack.id) }, + modifier = Modifier.constrainAs(quantity) { + baseline.linkTo(price.baseline) + end.linkTo(parent.end) + } + ) + JetsnackDivider( + Modifier.constrainAs(divider) { + linkTo(start = parent.start, end = parent.end) + top.linkTo(parent.bottom) + } + ) + } +} + +@Composable +private fun CartPreview() { + JetsnackTheme { + Cart( + orderLines = SnackRepo.getCart(), + removeSnack = {}, + increaseItemCount = {}, + decreaseItemCount = {}, + inspiredByCart = SnackRepo.getInspiredByCart(), + onSnackClick = {} + ) + } +} + +@Composable +actual fun rememberQuantityString(res: Int, qty: Int, vararg args: Any): String { + val resources = LocalContext.current.resources + return remember(qty, resources) { resources.getQuantityString(R.plurals.cart_order_count, qty, qty) } +} + +@Composable +actual fun getCartContentInsets(): WindowInsets { + return WindowInsets.statusBars.add(WindowInsets(top = 56.dp)) +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.android.kt b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.android.kt new file mode 100644 index 0000000000..e0c5f8f018 --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.android.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.home.cart + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.example.jetsnack.model.OrderLine +import com.example.jetsnack.model.SnackRepo +import com.example.jetsnack.model.SnackbarManager +import kotlinx.coroutines.flow.StateFlow + +/** + * Factory for CartViewModel that takes SnackbarManager as a dependency + */ +fun CartViewModel.Companion.provideFactory( + snackbarManager: SnackbarManager = SnackbarManager, + snackRepository: SnackRepo = SnackRepo +): ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return CartViewModel(snackbarManager, snackRepository) as T + } +} + +@OptIn(kotlin.ExperimentalMultiplatform::class) +actual abstract class JetSnackCartViewModel actual constructor() : ViewModel() { + @Composable + actual fun collectOrderLinesAsState(flow: StateFlow>): State> { + return flow.collectAsStateWithLifecycle() + } +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/snackCollectionListItemWindowInsets.kt b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/snackCollectionListItemWindowInsets.kt new file mode 100644 index 0000000000..ea94ef244e --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/home/snackCollectionListItemWindowInsets.kt @@ -0,0 +1,12 @@ +package com.example.jetsnack.ui.home + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.statusBars +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp + +@Composable +actual fun snackCollectionListItemWindowInsets(): WindowInsets { + return WindowInsets.statusBars.add(WindowInsets(top = 56.dp)) +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/snackdetail/jetSnackNavigationBarsPadding.kt b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/snackdetail/jetSnackNavigationBarsPadding.kt new file mode 100644 index 0000000000..c2a4b7224f --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/snackdetail/jetSnackNavigationBarsPadding.kt @@ -0,0 +1,10 @@ +package com.example.jetsnack.ui.snackdetail + +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.ui.Modifier + +actual fun Modifier.jetSnackNavigationBarsPadding(): Modifier = this.navigationBarsPadding() +actual fun Modifier.jetSnackStatusBarsPadding(): Modifier = this.statusBarsPadding() +actual fun Modifier.jetSnackSystemBarsPadding(): Modifier = this.systemBarsPadding() \ No newline at end of file diff --git a/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.kt b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.kt new file mode 100644 index 0000000000..c3dc9dfbaa --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.kt @@ -0,0 +1,10 @@ +package com.example.jetsnack.ui.utils + +import java.math.BigDecimal +import java.text.NumberFormat + +actual fun formatPrice(price: Long): String { + return NumberFormat.getCurrencyInstance().format( + BigDecimal(price).movePointLeft(2) + ) +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/androidMain/res/drawable-night/empty_state_search.xml b/examples/jetsnack/common/src/androidMain/res/drawable-night/empty_state_search.xml new file mode 100644 index 0000000000..1cd280e07a --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/res/drawable-night/empty_state_search.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/examples/jetsnack/common/src/androidMain/res/drawable-nodpi/placeholder.jpg b/examples/jetsnack/common/src/androidMain/res/drawable-nodpi/placeholder.jpg new file mode 100644 index 0000000000..26617a9595 Binary files /dev/null and b/examples/jetsnack/common/src/androidMain/res/drawable-nodpi/placeholder.jpg differ diff --git a/examples/jetsnack/common/src/androidMain/res/drawable-v26/ic_launcher_foreground.xml b/examples/jetsnack/common/src/androidMain/res/drawable-v26/ic_launcher_foreground.xml new file mode 100644 index 0000000000..6e2c52363d --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/res/drawable-v26/ic_launcher_foreground.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/examples/jetsnack/common/src/androidMain/res/drawable/empty_state_search.xml b/examples/jetsnack/common/src/androidMain/res/drawable/empty_state_search.xml new file mode 100644 index 0000000000..e2fa0cdac8 --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/res/drawable/empty_state_search.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/examples/jetsnack/common/src/androidMain/res/font/karla_bold.ttf b/examples/jetsnack/common/src/androidMain/res/font/karla_bold.ttf new file mode 100644 index 0000000000..052231c165 Binary files /dev/null and b/examples/jetsnack/common/src/androidMain/res/font/karla_bold.ttf differ diff --git a/examples/jetsnack/common/src/androidMain/res/font/karla_regular.ttf b/examples/jetsnack/common/src/androidMain/res/font/karla_regular.ttf new file mode 100644 index 0000000000..4269aa069e Binary files /dev/null and b/examples/jetsnack/common/src/androidMain/res/font/karla_regular.ttf differ diff --git a/examples/jetsnack/common/src/androidMain/res/font/montserrat_light.ttf b/examples/jetsnack/common/src/androidMain/res/font/montserrat_light.ttf new file mode 100644 index 0000000000..990857de8e Binary files /dev/null and b/examples/jetsnack/common/src/androidMain/res/font/montserrat_light.ttf differ diff --git a/examples/jetsnack/common/src/androidMain/res/font/montserrat_medium.ttf b/examples/jetsnack/common/src/androidMain/res/font/montserrat_medium.ttf new file mode 100644 index 0000000000..6e079f6984 Binary files /dev/null and b/examples/jetsnack/common/src/androidMain/res/font/montserrat_medium.ttf differ diff --git a/examples/jetsnack/common/src/androidMain/res/font/montserrat_regular.ttf b/examples/jetsnack/common/src/androidMain/res/font/montserrat_regular.ttf new file mode 100644 index 0000000000..8d443d5d56 Binary files /dev/null and b/examples/jetsnack/common/src/androidMain/res/font/montserrat_regular.ttf differ diff --git a/examples/jetsnack/common/src/androidMain/res/font/montserrat_semibold.ttf b/examples/jetsnack/common/src/androidMain/res/font/montserrat_semibold.ttf new file mode 100644 index 0000000000..f8a43f2b20 Binary files /dev/null and b/examples/jetsnack/common/src/androidMain/res/font/montserrat_semibold.ttf differ diff --git a/examples/jetsnack/common/src/androidMain/res/values-night/themes.xml b/examples/jetsnack/common/src/androidMain/res/values-night/themes.xml new file mode 100644 index 0000000000..d444fdea51 --- /dev/null +++ b/examples/jetsnack/common/src/androidMain/res/values-night/themes.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/AndroidxComposeMaterialIcons.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/AndroidxComposeMaterialIcons.kt new file mode 100644 index 0000000000..5740149512 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/AndroidxComposeMaterialIcons.kt @@ -0,0 +1,220 @@ +package com.example.jetsnack + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.materialIcon +import androidx.compose.material.icons.materialPath +import androidx.compose.ui.graphics.vector.ImageVector + +public val Icons.Filled.Android: ImageVector + get() { + if (_android != null) { + return _android!! + } + _android = materialIcon(name = "Filled.Android") { + materialPath { + moveTo(17.6f, 9.48f) + lineToRelative(1.84f, -3.18f) + curveToRelative(0.16f, -0.31f, 0.04f, -0.69f, -0.26f, -0.85f) + curveToRelative(-0.29f, -0.15f, -0.65f, -0.06f, -0.83f, 0.22f) + lineToRelative(-1.88f, 3.24f) + curveToRelative(-2.86f, -1.21f, -6.08f, -1.21f, -8.94f, 0.0f) + lineTo(5.65f, 5.67f) + curveToRelative(-0.19f, -0.29f, -0.58f, -0.38f, -0.87f, -0.2f) + curveTo(4.5f, 5.65f, 4.41f, 6.01f, 4.56f, 6.3f) + lineTo(6.4f, 9.48f) + curveTo(3.3f, 11.25f, 1.28f, 14.44f, 1.0f, 18.0f) + horizontalLineToRelative(22.0f) + curveTo(22.72f, 14.44f, 20.7f, 11.25f, 17.6f, 9.48f) + close() + moveTo(7.0f, 15.25f) + curveToRelative(-0.69f, 0.0f, -1.25f, -0.56f, -1.25f, -1.25f) + curveToRelative(0.0f, -0.69f, 0.56f, -1.25f, 1.25f, -1.25f) + reflectiveCurveTo(8.25f, 13.31f, 8.25f, 14.0f) + curveTo(8.25f, 14.69f, 7.69f, 15.25f, 7.0f, 15.25f) + close() + moveTo(17.0f, 15.25f) + curveToRelative(-0.69f, 0.0f, -1.25f, -0.56f, -1.25f, -1.25f) + curveToRelative(0.0f, -0.69f, 0.56f, -1.25f, 1.25f, -1.25f) + reflectiveCurveToRelative(1.25f, 0.56f, 1.25f, 1.25f) + curveTo(18.25f, 14.69f, 17.69f, 15.25f, 17.0f, 15.25f) + close() + } + } + return _android!! + } + +private var _android: ImageVector? = null + +public val Icons.Filled.SortByAlpha: ImageVector + get() { + if (_sortByAlpha != null) { + return _sortByAlpha!! + } + _sortByAlpha = materialIcon(name = "Filled.SortByAlpha") { + materialPath { + moveTo(14.94f, 4.66f) + horizontalLineToRelative(-4.72f) + lineToRelative(2.36f, -2.36f) + close() + moveTo(10.25f, 19.37f) + horizontalLineToRelative(4.66f) + lineToRelative(-2.33f, 2.33f) + close() + moveTo(6.1f, 6.27f) + lineTo(1.6f, 17.73f) + horizontalLineToRelative(1.84f) + lineToRelative(0.92f, -2.45f) + horizontalLineToRelative(5.11f) + lineToRelative(0.92f, 2.45f) + horizontalLineToRelative(1.84f) + lineTo(7.74f, 6.27f) + lineTo(6.1f, 6.27f) + close() + moveTo(4.97f, 13.64f) + lineToRelative(1.94f, -5.18f) + lineToRelative(1.94f, 5.18f) + lineTo(4.97f, 13.64f) + close() + moveTo(15.73f, 16.14f) + horizontalLineToRelative(6.12f) + verticalLineToRelative(1.59f) + horizontalLineToRelative(-8.53f) + verticalLineToRelative(-1.29f) + lineToRelative(5.92f, -8.56f) + horizontalLineToRelative(-5.88f) + verticalLineToRelative(-1.6f) + horizontalLineToRelative(8.3f) + verticalLineToRelative(1.26f) + lineToRelative(-5.93f, 8.6f) + close() + } + } + return _sortByAlpha!! + } + +private var _sortByAlpha: ImageVector? = null + +public val Icons.Rounded.FilterList: ImageVector + get() { + if (_filterList != null) { + return _filterList!! + } + _filterList = materialIcon(name = "Rounded.FilterList") { + materialPath { + moveTo(11.0f, 18.0f) + horizontalLineToRelative(2.0f) + curveToRelative(0.55f, 0.0f, 1.0f, -0.45f, 1.0f, -1.0f) + reflectiveCurveToRelative(-0.45f, -1.0f, -1.0f, -1.0f) + horizontalLineToRelative(-2.0f) + curveToRelative(-0.55f, 0.0f, -1.0f, 0.45f, -1.0f, 1.0f) + reflectiveCurveToRelative(0.45f, 1.0f, 1.0f, 1.0f) + close() + moveTo(3.0f, 7.0f) + curveToRelative(0.0f, 0.55f, 0.45f, 1.0f, 1.0f, 1.0f) + horizontalLineToRelative(16.0f) + curveToRelative(0.55f, 0.0f, 1.0f, -0.45f, 1.0f, -1.0f) + reflectiveCurveToRelative(-0.45f, -1.0f, -1.0f, -1.0f) + lineTo(4.0f, 6.0f) + curveToRelative(-0.55f, 0.0f, -1.0f, 0.45f, -1.0f, 1.0f) + close() + moveTo(7.0f, 13.0f) + horizontalLineToRelative(10.0f) + curveToRelative(0.55f, 0.0f, 1.0f, -0.45f, 1.0f, -1.0f) + reflectiveCurveToRelative(-0.45f, -1.0f, -1.0f, -1.0f) + lineTo(7.0f, 11.0f) + curveToRelative(-0.55f, 0.0f, -1.0f, 0.45f, -1.0f, 1.0f) + reflectiveCurveToRelative(0.45f, 1.0f, 1.0f, 1.0f) + close() + } + } + return _filterList!! + } + +private var _filterList: ImageVector? = null + +public val Icons.Filled.Remove: ImageVector + get() { + if (_remove != null) { + return _remove!! + } + _remove = materialIcon(name = "Filled.Remove") { + materialPath { + moveTo(19.0f, 13.0f) + horizontalLineTo(5.0f) + verticalLineToRelative(-2.0f) + horizontalLineToRelative(14.0f) + verticalLineToRelative(2.0f) + close() + } + } + return _remove!! + } + +private var _remove: ImageVector? = null + +public val Icons.Outlined.ExpandMore: ImageVector + get() { + if (_expandMore != null) { + return _expandMore!! + } + _expandMore = materialIcon(name = "Outlined.ExpandMore") { + materialPath { + moveTo(16.59f, 8.59f) + lineTo(12.0f, 13.17f) + lineTo(7.41f, 8.59f) + lineTo(6.0f, 10.0f) + lineToRelative(6.0f, 6.0f) + lineToRelative(6.0f, -6.0f) + lineToRelative(-1.41f, -1.41f) + close() + } + } + return _expandMore!! + } + +private var _expandMore: ImageVector? = null + +public val Icons.Filled.DeleteForever: ImageVector + get() { + if (_deleteForever != null) { + return _deleteForever!! + } + _deleteForever = materialIcon(name = "Filled.DeleteForever") { + materialPath { + moveTo(6.0f, 19.0f) + curveToRelative(0.0f, 1.1f, 0.9f, 2.0f, 2.0f, 2.0f) + horizontalLineToRelative(8.0f) + curveToRelative(1.1f, 0.0f, 2.0f, -0.9f, 2.0f, -2.0f) + lineTo(18.0f, 7.0f) + lineTo(6.0f, 7.0f) + verticalLineToRelative(12.0f) + close() + moveTo(8.46f, 11.88f) + lineToRelative(1.41f, -1.41f) + lineTo(12.0f, 12.59f) + lineToRelative(2.12f, -2.12f) + lineToRelative(1.41f, 1.41f) + lineTo(13.41f, 14.0f) + lineToRelative(2.12f, 2.12f) + lineToRelative(-1.41f, 1.41f) + lineTo(12.0f, 15.41f) + lineToRelative(-2.12f, 2.12f) + lineToRelative(-1.41f, -1.41f) + lineTo(10.59f, 14.0f) + lineToRelative(-2.13f, -2.12f) + close() + moveTo(15.5f, 4.0f) + lineToRelative(-1.0f, -1.0f) + horizontalLineToRelative(-5.0f) + lineToRelative(-1.0f, 1.0f) + lineTo(5.0f, 4.0f) + verticalLineToRelative(2.0f) + horizontalLineToRelative(14.0f) + lineTo(19.0f, 4.0f) + close() + } + } + return _deleteForever!! + } + +private var _deleteForever: ImageVector? = null \ No newline at end of file diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/DrawableRes.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/DrawableRes.kt new file mode 100644 index 0000000000..5d2f3a27e0 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/DrawableRes.kt @@ -0,0 +1,9 @@ +package com.example.jetsnack + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.painter.Painter + +@Composable +expect fun painterResource(id: Int): Painter + +expect val MppR.drawable.empty_state_search: Int \ No newline at end of file diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/StringRes.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/StringRes.kt new file mode 100644 index 0000000000..b91177b878 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/StringRes.kt @@ -0,0 +1,87 @@ +package com.example.jetsnack + +import androidx.compose.runtime.Composable + +@Composable +expect fun stringResource(id: Int): String + +@Composable +expect fun stringResource(id: Int, part: String): String + +@Composable +expect fun stringResource(id: Int, count: Int): String + +object MppR { + object string {} + + object drawable {} + + object plurals {} +} + +expect val MppR.plurals.cart_order_count: Int + +// Filters +expect val MppR.string.label_filters: Int + +// Qty +expect val MppR.string.quantity: Int +expect val MppR.string.label_decrease: Int +expect val MppR.string.label_increase: Int + + +// Snack detail +expect val MppR.string.label_back: Int +expect val MppR.string.detail_header: Int +expect val MppR.string.detail_placeholder: Int +expect val MppR.string.see_more: Int +expect val MppR.string.see_less: Int +expect val MppR.string.ingredients: Int +expect val MppR.string.ingredients_list: Int +expect val MppR.string.add_to_cart: Int + +// Home + +expect val MppR.string.label_select_delivery: Int + +// Filter +expect val MppR.string.max_calories: Int +expect val MppR.string.per_serving: Int +expect val MppR.string.sort: Int +expect val MppR.string.lifestyle: Int +expect val MppR.string.category: Int +expect val MppR.string.price: Int +expect val MppR.string.reset: Int +expect val MppR.string.close: Int + +// Profile + +expect val MppR.string.work_in_progress: Int +expect val MppR.string.grab_beverage: Int + +// Home +expect val MppR.string.home_feed: Int +expect val MppR.string.home_search: Int +expect val MppR.string.home_cart: Int +expect val MppR.string.home_profile: Int + +// Search +expect val MppR.string.search_no_matches: Int +expect val MppR.string.search_no_matches_retry: Int +expect val MppR.string.label_add: Int +expect val MppR.string.search_count: Int +expect val MppR.string.label_search: Int +expect val MppR.string.search_jetsnack: Int + +expect val MppR.string.cart_increase_error: Int +expect val MppR.string.cart_decrease_error: Int + +// Cart +expect val MppR.string.cart_order_header: Int +expect val MppR.string.remove_item: Int +expect val MppR.string.cart_summary_header: Int +expect val MppR.string.cart_subtotal_label: Int +expect val MppR.string.cart_shipping_label: Int +expect val MppR.string.cart_total_label: Int +expect val MppR.string.cart_checkout: Int +expect val MppR.string.label_remove: Int \ No newline at end of file diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/flowlayout/FlowAccompanist.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/flowlayout/FlowAccompanist.kt new file mode 100644 index 0000000000..d3b87abc7a --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/flowlayout/FlowAccompanist.kt @@ -0,0 +1,337 @@ +package com.example.jetsnack.flowlayout + + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import kotlin.math.max + +internal enum class LayoutOrientation { + Horizontal, + Vertical +} + +internal data class OrientationIndependentConstraints( + val mainAxisMin: Int, + val mainAxisMax: Int, + val crossAxisMin: Int, + val crossAxisMax: Int +) { + constructor(c: Constraints, orientation: LayoutOrientation) : this( + if (orientation === LayoutOrientation.Horizontal) c.minWidth else c.minHeight, + if (orientation === LayoutOrientation.Horizontal) c.maxWidth else c.maxHeight, + if (orientation === LayoutOrientation.Horizontal) c.minHeight else c.minWidth, + if (orientation === LayoutOrientation.Horizontal) c.maxHeight else c.maxWidth + ) +} + +/** + * A composable that places its children in a horizontal flow. Unlike [Row], if the + * horizontal space is too small to put all the children in one row, multiple rows may be used. + * + * Note that just like [Row], flex values cannot be used with [FlowRow]. + * + * @param modifier The modifier to be applied to the FlowRow. + * @param mainAxisSize The size of the layout in the main axis direction. + * @param mainAxisAlignment The alignment of each row's children in the main axis direction. + * @param mainAxisSpacing The main axis spacing between the children of each row. + * @param crossAxisAlignment The alignment of each row's children in the cross axis direction. + * @param crossAxisSpacing The cross axis spacing between the rows of the layout. + * @param lastLineMainAxisAlignment Overrides the main axis alignment of the last row. + */ +@Composable +public fun FlowRow( + modifier: Modifier = Modifier, + mainAxisSize: SizeMode = SizeMode.Wrap, + mainAxisAlignment: FlowMainAxisAlignment = FlowMainAxisAlignment.Start, + mainAxisSpacing: Dp = 0.dp, + crossAxisAlignment: FlowCrossAxisAlignment = FlowCrossAxisAlignment.Start, + crossAxisSpacing: Dp = 0.dp, + lastLineMainAxisAlignment: FlowMainAxisAlignment = mainAxisAlignment, + content: @Composable () -> Unit +) { + Flow( + modifier = modifier, + orientation = LayoutOrientation.Horizontal, + mainAxisSize = mainAxisSize, + mainAxisAlignment = mainAxisAlignment, + mainAxisSpacing = mainAxisSpacing, + crossAxisAlignment = crossAxisAlignment, + crossAxisSpacing = crossAxisSpacing, + lastLineMainAxisAlignment = lastLineMainAxisAlignment, + content = content + ) +} + +/** + * A composable that places its children in a vertical flow. Unlike [Column], if the + * vertical space is too small to put all the children in one column, multiple columns may be used. + * + * Note that just like [Column], flex values cannot be used with [FlowColumn]. + * + * @param modifier The modifier to be applied to the FlowColumn. + * @param mainAxisSize The size of the layout in the main axis direction. + * @param mainAxisAlignment The alignment of each column's children in the main axis direction. + * @param mainAxisSpacing The main axis spacing between the children of each column. + * @param crossAxisAlignment The alignment of each column's children in the cross axis direction. + * @param crossAxisSpacing The cross axis spacing between the columns of the layout. + * @param lastLineMainAxisAlignment Overrides the main axis alignment of the last column. + */ +@Composable +public fun FlowColumn( + modifier: Modifier = Modifier, + mainAxisSize: SizeMode = SizeMode.Wrap, + mainAxisAlignment: FlowMainAxisAlignment = FlowMainAxisAlignment.Start, + mainAxisSpacing: Dp = 0.dp, + crossAxisAlignment: FlowCrossAxisAlignment = FlowCrossAxisAlignment.Start, + crossAxisSpacing: Dp = 0.dp, + lastLineMainAxisAlignment: FlowMainAxisAlignment = mainAxisAlignment, + content: @Composable () -> Unit +) { + Flow( + modifier = modifier, + orientation = LayoutOrientation.Vertical, + mainAxisSize = mainAxisSize, + mainAxisAlignment = mainAxisAlignment, + mainAxisSpacing = mainAxisSpacing, + crossAxisAlignment = crossAxisAlignment, + crossAxisSpacing = crossAxisSpacing, + lastLineMainAxisAlignment = lastLineMainAxisAlignment, + content = content + ) +} + +/** + * Used to specify the alignment of a layout's children, in cross axis direction. + */ +public enum class FlowCrossAxisAlignment { + /** + * Place children such that their center is in the middle of the cross axis. + */ + Center, + /** + * Place children such that their start edge is aligned to the start edge of the cross axis. + */ + Start, + /** + * Place children such that their end edge is aligned to the end edge of the cross axis. + */ + End, +} + +public typealias FlowMainAxisAlignment = MainAxisAlignment + +/** + * Layout model that arranges its children in a horizontal or vertical flow. + */ +@Composable +private fun Flow( + modifier: Modifier, + orientation: LayoutOrientation, + mainAxisSize: SizeMode, + mainAxisAlignment: FlowMainAxisAlignment, + mainAxisSpacing: Dp, + crossAxisAlignment: FlowCrossAxisAlignment, + crossAxisSpacing: Dp, + lastLineMainAxisAlignment: FlowMainAxisAlignment, + content: @Composable () -> Unit +) { + fun Placeable.mainAxisSize() = + if (orientation == LayoutOrientation.Horizontal) width else height + fun Placeable.crossAxisSize() = + if (orientation == LayoutOrientation.Horizontal) height else width + + Layout(content, modifier) { measurables, outerConstraints -> + val sequences = mutableListOf>() + val crossAxisSizes = mutableListOf() + val crossAxisPositions = mutableListOf() + + var mainAxisSpace = 0 + var crossAxisSpace = 0 + + val currentSequence = mutableListOf() + var currentMainAxisSize = 0 + var currentCrossAxisSize = 0 + + val constraints = OrientationIndependentConstraints(outerConstraints, orientation) + + val childConstraints = if (orientation == LayoutOrientation.Horizontal) { + Constraints(maxWidth = constraints.mainAxisMax) + } else { + Constraints(maxHeight = constraints.mainAxisMax) + } + + // Return whether the placeable can be added to the current sequence. + fun canAddToCurrentSequence(placeable: Placeable) = + currentSequence.isEmpty() || currentMainAxisSize + mainAxisSpacing.roundToPx() + + placeable.mainAxisSize() <= constraints.mainAxisMax + + // Store current sequence information and start a new sequence. + fun startNewSequence() { + if (sequences.isNotEmpty()) { + crossAxisSpace += crossAxisSpacing.roundToPx() + } + sequences += currentSequence.toList() + crossAxisSizes += currentCrossAxisSize + crossAxisPositions += crossAxisSpace + + crossAxisSpace += currentCrossAxisSize + mainAxisSpace = max(mainAxisSpace, currentMainAxisSize) + + currentSequence.clear() + currentMainAxisSize = 0 + currentCrossAxisSize = 0 + } + + for (measurable in measurables) { + // Ask the child for its preferred size. + val placeable = measurable.measure(childConstraints) + + // Start a new sequence if there is not enough space. + if (!canAddToCurrentSequence(placeable)) startNewSequence() + + // Add the child to the current sequence. + if (currentSequence.isNotEmpty()) { + currentMainAxisSize += mainAxisSpacing.roundToPx() + } + currentSequence.add(placeable) + currentMainAxisSize += placeable.mainAxisSize() + currentCrossAxisSize = max(currentCrossAxisSize, placeable.crossAxisSize()) + } + + if (currentSequence.isNotEmpty()) startNewSequence() + + val mainAxisLayoutSize = if (constraints.mainAxisMax != Constraints.Infinity && + mainAxisSize == SizeMode.Expand + ) { + constraints.mainAxisMax + } else { + max(mainAxisSpace, constraints.mainAxisMin) + } + val crossAxisLayoutSize = max(crossAxisSpace, constraints.crossAxisMin) + + val layoutWidth = if (orientation == LayoutOrientation.Horizontal) { + mainAxisLayoutSize + } else { + crossAxisLayoutSize + } + val layoutHeight = if (orientation == LayoutOrientation.Horizontal) { + crossAxisLayoutSize + } else { + mainAxisLayoutSize + } + + layout(layoutWidth, layoutHeight) { + sequences.forEachIndexed { i, placeables -> + val childrenMainAxisSizes = IntArray(placeables.size) { j -> + placeables[j].mainAxisSize() + + if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0 + } + val arrangement = if (i < sequences.lastIndex) { + mainAxisAlignment.arrangement + } else { + lastLineMainAxisAlignment.arrangement + } + // TODO(soboleva): rtl support + // Handle vertical direction + val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 } + with(arrangement) { + arrange(mainAxisLayoutSize, childrenMainAxisSizes, mainAxisPositions) + } + placeables.forEachIndexed { j, placeable -> + val crossAxis = when (crossAxisAlignment) { + FlowCrossAxisAlignment.Start -> 0 + FlowCrossAxisAlignment.End -> + crossAxisSizes[i] - placeable.crossAxisSize() + FlowCrossAxisAlignment.Center -> + Alignment.Center.align( + IntSize.Zero, + IntSize( + width = 0, + height = crossAxisSizes[i] - placeable.crossAxisSize() + ), + LayoutDirection.Ltr + ).y + } + if (orientation == LayoutOrientation.Horizontal) { + placeable.place( + x = mainAxisPositions[j], + y = crossAxisPositions[i] + crossAxis + ) + } else { + placeable.place( + x = crossAxisPositions[i] + crossAxis, + y = mainAxisPositions[j] + ) + } + } + } + } + } +} + +/** + * Used to specify how a layout chooses its own size when multiple behaviors are possible. + */ +// TODO(popam): remove this when Flow is reworked +public enum class SizeMode { + /** + * Minimize the amount of free space by wrapping the children, + * subject to the incoming layout constraints. + */ + Wrap, + /** + * Maximize the amount of free space by expanding to fill the available space, + * subject to the incoming layout constraints. + */ + Expand +} + +/** + * Used to specify the alignment of a layout's children, in main axis direction. + */ +public enum class MainAxisAlignment(internal val arrangement: Arrangement.Vertical) { + // TODO(soboleva) support RTl in Flow + // workaround for now - use Arrangement that equals to previous Arrangement + /** + * Place children such that they are as close as possible to the middle of the main axis. + */ + Center(Arrangement.Center), + + /** + * Place children such that they are as close as possible to the start of the main axis. + */ + Start(Arrangement.Top), + + /** + * Place children such that they are as close as possible to the end of the main axis. + */ + End(Arrangement.Bottom), + + /** + * Place children such that they are spaced evenly across the main axis, including free + * space before the first child and after the last child. + */ + SpaceEvenly(Arrangement.SpaceEvenly), + + /** + * Place children such that they are spaced evenly across the main axis, without free + * space before the first child or after the last child. + */ + SpaceBetween(Arrangement.SpaceBetween), + + /** + * Place children such that they are spaced evenly across the main axis, including free + * space before the first child and after the last child, but half the amount of space + * existing otherwise between two consecutive children. + */ + SpaceAround(Arrangement.SpaceAround); +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Filter.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Filter.kt new file mode 100644 index 0000000000..37a9599978 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Filter.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.model + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.vector.ImageVector +import com.example.jetsnack.Android +import com.example.jetsnack.SortByAlpha + +@Stable +class Filter( + val name: String, + enabled: Boolean = false, + val icon: ImageVector? = null +) { + val enabled = mutableStateOf(enabled) +} +val filters = listOf( + Filter(name = "Organic"), + Filter(name = "Gluten-free"), + Filter(name = "Dairy-free"), + Filter(name = "Sweet"), + Filter(name = "Savory") +) +val priceFilters = listOf( + Filter(name = "$"), + Filter(name = "$$"), + Filter(name = "$$$"), + Filter(name = "$$$$") +) +val sortFilters = listOf( + Filter(name = "Android's favorite (default)", icon = Icons.Filled.Android), + Filter(name = "Rating", icon = Icons.Filled.Star), + Filter(name = "Alphabetical", icon = Icons.Filled.SortByAlpha) +) + +val categoryFilters = listOf( + Filter(name = "Chips & crackers"), + Filter(name = "Fruit snacks"), + Filter(name = "Desserts"), + Filter(name = "Nuts") +) +val lifeStyleFilters = listOf( + Filter(name = "Organic"), + Filter(name = "Gluten-free"), + Filter(name = "Dairy-free"), + Filter(name = "Sweet"), + Filter(name = "Savory") +) + +var sortDefault = sortFilters.get(0).name diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Search.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Search.kt new file mode 100644 index 0000000000..d22a41edb4 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Search.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.model + +import androidx.compose.runtime.Immutable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext + +/** + * A fake repo for searching. + */ +object SearchRepo { + fun getCategories(): List = searchCategoryCollections + fun getSuggestions(): List = searchSuggestions + + suspend fun search(query: String): List = withContext(Dispatchers.Default) { + delay(200L) // simulate an I/O delay + snacks.filter { it.name.contains(query, ignoreCase = true) } + } +} + +@Immutable +data class SearchCategoryCollection( + val id: Long, + val name: String, + val categories: List +) + +@Immutable +data class SearchCategory( + val name: String, + val imageUrl: String +) + +@Immutable +data class SearchSuggestionGroup( + val id: Long, + val name: String, + val suggestions: List +) + +/** + * Static data + */ + +private val searchCategoryCollections = listOf( + SearchCategoryCollection( + id = 0L, + name = "Categories", + categories = listOf( + SearchCategory( + name = "Chips & crackers", + imageUrl = "https://source.unsplash.com/UsSdMZ78Q3E" + ), + SearchCategory( + name = "Fruit snacks", + imageUrl = "https://source.unsplash.com/SfP1PtM9Qa8" + ), + SearchCategory( + name = "Desserts", + imageUrl = "https://source.unsplash.com/_jk8KIyN_uA" + ), + SearchCategory( + name = "Nuts ", + imageUrl = "https://source.unsplash.com/UsSdMZ78Q3E" + ) + ) + ), + SearchCategoryCollection( + id = 1L, + name = "Lifestyles", + categories = listOf( + SearchCategory( + name = "Organic", + imageUrl = "https://source.unsplash.com/7meCnGCJ5Ms" + ), + SearchCategory( + name = "Gluten Free", + imageUrl = "https://source.unsplash.com/m741tj4Cz7M" + ), + SearchCategory( + name = "Paleo", + imageUrl = "https://source.unsplash.com/dt5-8tThZKg" + ), + SearchCategory( + name = "Vegan", + imageUrl = "https://source.unsplash.com/ReXxkS1m1H0" + ), + SearchCategory( + name = "Vegitarian", + imageUrl = "https://source.unsplash.com/IGfIGP5ONV0" + ), + SearchCategory( + name = "Whole30", + imageUrl = "https://source.unsplash.com/9MzCd76xLGk" + ) + ) + ) +) + +private val searchSuggestions = listOf( + SearchSuggestionGroup( + id = 0L, + name = "Recent searches", + suggestions = listOf( + "Cheese", + "Apple Sauce" + ) + ), + SearchSuggestionGroup( + id = 1L, + name = "Popular searches", + suggestions = listOf( + "Organic", + "Gluten Free", + "Paleo", + "Vegan", + "Vegitarian", + "Whole30" + ) + ) +) diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Snack.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Snack.kt new file mode 100644 index 0000000000..ae2ddb00cc --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/Snack.kt @@ -0,0 +1,226 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.model + +import androidx.compose.runtime.Immutable + +@Immutable +data class Snack( + val id: Long, + val name: String, + val imageUrl: String, + val price: Long, + val tagline: String = "", + val tags: Set = emptySet() +) + +/** + * Static data + */ + +val snacks = listOf( + Snack( + id = 1L, + name = "Cupcake", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/pGM4sjt_BdQ", + price = 299 + ), + Snack( + id = 2L, + name = "Donut", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/Yc5sL-ejk6U", + price = 290 + ), + Snack( + id = 3L, + name = "Eclair", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/-LojFX9NfPY", + price = 289 + ), + Snack( + id = 4L, + name = "Froyo", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/3U2V5WqK1PQ", + price = 288 + ), + Snack( + id = 5L, + name = "Gingerbread", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/Y4YR9OjdIMk", + price = 499 + ), + Snack( + id = 6L, + name = "Honeycomb", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/bELvIg_KZGU", + price = 309 + ), + Snack( + id = 7L, + name = "Ice Cream Sandwich", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/YgYJsFDd4AU", + price = 1299 + ), + Snack( + id = 8L, + name = "Jellybean", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/0u_vbeOkMpk", + price = 109 + ), + Snack( + id = 9L, + name = "KitKat", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/yb16pT5F_jE", + price = 549 + ), + Snack( + id = 10L, + name = "Lollipop", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/AHF_ZktTL6Q", + price = 209 + ), + Snack( + id = 11L, + name = "Marshmallow", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/rqFm0IgMVYY", + price = 219 + ), + Snack( + id = 12L, + name = "Nougat", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/qRE_OpbVPR8", + price = 309 + ), + Snack( + id = 13L, + name = "Oreo", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/33fWPnyN6tU", + price = 339 + ), + Snack( + id = 14L, + name = "Pie", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/aX_ljOOyWJY", + price = 249 + ), + Snack( + id = 15L, + name = "Chips", + imageUrl = "https://source.unsplash.com/UsSdMZ78Q3E", + price = 277 + ), + Snack( + id = 16L, + name = "Pretzels", + imageUrl = "https://source.unsplash.com/7meCnGCJ5Ms", + price = 154 + ), + Snack( + id = 17L, + name = "Smoothies", + imageUrl = "https://source.unsplash.com/m741tj4Cz7M", + price = 257 + ), + Snack( + id = 18L, + name = "Popcorn", + imageUrl = "https://source.unsplash.com/iuwMdNq0-s4", + price = 167 + ), + Snack( + id = 19L, + name = "Almonds", + imageUrl = "https://source.unsplash.com/qgWWQU1SzqM", + price = 123 + ), + Snack( + id = 20L, + name = "Cheese", + imageUrl = "https://source.unsplash.com/9MzCd76xLGk", + price = 231 + ), + Snack( + id = 21L, + name = "Apples", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/1d9xXWMtQzQ", + price = 221 + ), + Snack( + id = 22L, + name = "Apple sauce", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/wZxpOw84QTU", + price = 222 + ), + Snack( + id = 23L, + name = "Apple chips", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/okzeRxm_GPo", + price = 231 + ), + Snack( + id = 24L, + name = "Apple juice", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/l7imGdupuhU", + price = 241 + ), + Snack( + id = 25L, + name = "Apple pie", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/bkXzABDt08Q", + price = 225 + ), + Snack( + id = 26L, + name = "Grapes", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/y2MeW00BdBo", + price = 266 + ), + Snack( + id = 27L, + name = "Kiwi", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/1oMGgHn-M8k", + price = 127 + ), + Snack( + id = 28L, + name = "Mango", + tagline = "A tag line", + imageUrl = "https://source.unsplash.com/TIGDsyy0TK4", + price = 128 + ) +) diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/SnackCollection.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/SnackCollection.kt new file mode 100644 index 0000000000..936d45d38b --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/SnackCollection.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.model + +import androidx.compose.runtime.Immutable + +@Immutable +data class SnackCollection( + val id: Long, + val name: String, + val snacks: List, + val type: CollectionType = CollectionType.Normal +) + +enum class CollectionType { Normal, Highlight } + +/** + * A fake repo + */ +object SnackRepo { + fun getSnacks(): List = snackCollections + fun getSnack(snackId: Long) = snacks.find { it.id == snackId }!! + fun getRelated(@Suppress("UNUSED_PARAMETER") snackId: Long) = related + fun getInspiredByCart() = inspiredByCart + fun getFilters() = filters + fun getPriceFilters() = priceFilters + fun getCart() = cart + fun getSortFilters() = sortFilters + fun getCategoryFilters() = categoryFilters + fun getSortDefault() = sortDefault + fun getLifeStyleFilters() = lifeStyleFilters +} + +/** + * Static data + */ + +private val tastyTreats = SnackCollection( + id = 1L, + name = "Android's picks", + type = CollectionType.Highlight, + snacks = snacks.subList(0, 13) +) + +private val popular = SnackCollection( + id = 2L, + name = "Popular on Jetsnack", + snacks = snacks.subList(14, 19) +) + +private val wfhFavs = tastyTreats.copy( + id = 3L, + name = "WFH favourites" +) + +private val newlyAdded = popular.copy( + id = 4L, + name = "Newly Added" +) + +private val exclusive = tastyTreats.copy( + id = 5L, + name = "Only on Jetsnack" +) + +private val also = tastyTreats.copy( + id = 6L, + name = "Customers also bought" +) + +private val inspiredByCart = tastyTreats.copy( + id = 7L, + name = "Inspired by your cart" +) + +private val snackCollections = listOf( + tastyTreats, + popular, + wfhFavs, + newlyAdded, + exclusive +) + +private val related = listOf( + also, + popular +) + +private val cart = listOf( + OrderLine(snacks[4], 2), + OrderLine(snacks[6], 3), + OrderLine(snacks[8], 1) +) + +@Immutable +data class OrderLine( + val snack: Snack, + val count: Int +) diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/SnackbarManager.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/SnackbarManager.kt new file mode 100644 index 0000000000..568aac92a1 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/model/SnackbarManager.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.model + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +data class Message(val id: Long, val message: Int /*@StringRes*/) + +/** + * Class responsible for managing Snackbar messages to show on the screen + */ +object SnackbarManager { + + private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) + val messages: StateFlow> get() = _messages.asStateFlow() + + fun showMessage(message: Int /*@StringRes*/) { + _messages.update { currentMessages -> + currentMessages + Message( + id = createRandomUUID(), + message = message + ) + } + } + + fun setMessageShown(messageId: Long) { + _messages.update { currentMessages -> + currentMessages.filterNot { it.id == messageId } + } + } +} + +expect fun createRandomUUID(): Long diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/JetsnackApp.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/JetsnackApp.kt new file mode 100644 index 0000000000..daeab90159 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/JetsnackApp.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material.SnackbarHost +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.example.jetsnack.ui.components.JetsnackScaffold +import com.example.jetsnack.ui.components.JetsnackSnackbar +import com.example.jetsnack.ui.home.JetsnackBottomBar +import com.example.jetsnack.ui.snackdetail.jetSnackSystemBarsPadding +import com.example.jetsnack.ui.theme.JetsnackTheme + +@Composable +fun JetsnackApp() { + JetsnackTheme { + val appState = rememberMppJetsnackAppState() + JetsnackScaffold( + bottomBar = { + if (appState.shouldShowBottomBar()) { + JetsnackBottomBar( + tabs = appState.bottomBarTabs, + currentRoute = appState.currentRoute!!, + navigateToRoute = appState::navigateToBottomBarRoute + ) + } + }, + snackbarHost = { + SnackbarHost( + hostState = it, + modifier = Modifier.jetSnackSystemBarsPadding(), + snackbar = { snackbarData -> JetsnackSnackbar(snackbarData) } + ) + }, + scaffoldState = appState.scaffoldState + ) { innerPaddingModifier -> + JetsnackScaffoldContent(innerPaddingModifier, appState) + } + } +} + +@Composable +expect fun JetsnackScaffoldContent(innerPaddingModifier: PaddingValues, appState: MppJetsnackAppState) + diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/JetsnackAppState.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/JetsnackAppState.kt new file mode 100644 index 0000000000..ede9a66ba9 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/JetsnackAppState.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui + +import androidx.compose.material.ScaffoldState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import com.example.jetsnack.model.SnackbarManager +import com.example.jetsnack.ui.home.HomeSections +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +/** + * Destinations used in the [JetsnackApp]. + */ +object MainDestinations { + const val HOME_ROUTE = "home" + const val SNACK_DETAIL_ROUTE = "snack" + const val SNACK_ID_KEY = "snackId" +} + + +@Composable +expect fun rememberMppJetsnackAppState(): MppJetsnackAppState + +@Stable +expect class MppJetsnackAppState { + + val scaffoldState: ScaffoldState + val snackbarManager: SnackbarManager + val coroutineScope: CoroutineScope + val bottomBarTabs: Array + 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 +//} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Button.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Button.kt new file mode 100644 index 0000000000..dd1889e39e --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Button.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.components + +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.semantics.Role +import com.example.jetsnack.ui.theme.JetsnackTheme + +@Composable + +fun JetsnackButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + shape: Shape = ButtonShape, + border: BorderStroke? = null, + backgroundGradient: List = JetsnackTheme.colors.interactivePrimary, + disabledBackgroundGradient: List = JetsnackTheme.colors.interactiveSecondary, + contentColor: Color = JetsnackTheme.colors.textInteractive, + disabledContentColor: Color = JetsnackTheme.colors.textHelp, + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + content: @Composable RowScope.() -> Unit +) { + JetsnackSurface( + shape = shape, + color = Color.Transparent, + contentColor = if (enabled) contentColor else disabledContentColor, + border = border, + modifier = modifier + .clip(shape) + .background( + Brush.horizontalGradient( + colors = if (enabled) backgroundGradient else disabledBackgroundGradient + ) + ) + .clickable( + onClick = onClick, + enabled = enabled, + role = Role.Button, + interactionSource = interactionSource, + indication = null + ) + ) { + ProvideTextStyle( + value = MaterialTheme.typography.button + ) { + Row( + Modifier + .defaultMinSize( + minWidth = ButtonDefaults.MinWidth, + minHeight = ButtonDefaults.MinHeight + ) + .indication(interactionSource, rememberRipple()) + .padding(contentPadding), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + content = content + ) + } + } +} + +private val ButtonShape = RoundedCornerShape(percent = 50) + +//@Preview +@Composable +private fun ButtonPreview() { + JetsnackTheme { + JetsnackButton(onClick = {}) { + Text(text = "Demo") + } + } +} + +//@Preview +@Composable +private fun RectangleButtonPreview() { + JetsnackTheme { + JetsnackButton( + onClick = {}, shape = RectangleShape + ) { + Text(text = "Demo") + } + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Card.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Card.kt new file mode 100644 index 0000000000..d2953e315c --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Card.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.components + +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.example.jetsnack.ui.theme.JetsnackTheme + +@Composable +fun JetsnackCard( + modifier: Modifier = Modifier, + shape: Shape = MaterialTheme.shapes.medium, + color: Color = JetsnackTheme.colors.uiBackground, + contentColor: Color = JetsnackTheme.colors.textPrimary, + border: BorderStroke? = null, + elevation: Dp = 4.dp, + content: @Composable () -> Unit +) { + JetsnackSurface( + modifier = modifier, + shape = shape, + color = color, + contentColor = contentColor, + elevation = elevation, + border = border, + content = content + ) +} + +//@Preview +@Composable +private fun CardPreview() { + JetsnackTheme { + JetsnackCard { + Text(text = "Demo", modifier = Modifier.padding(16.dp)) + } + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Divider.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Divider.kt new file mode 100644 index 0000000000..28dd6a84c2 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Divider.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.components + +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material.Divider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.example.jetsnack.ui.theme.JetsnackTheme + +@Composable +fun JetsnackDivider( + modifier: Modifier = Modifier, + color: Color = JetsnackTheme.colors.uiBorder.copy(alpha = DividerAlpha), + thickness: Dp = 1.dp, + startIndent: Dp = 0.dp +) { + Divider( + modifier = modifier, + color = color, + thickness = thickness, + startIndent = startIndent + ) +} + +private const val DividerAlpha = 0.12f + +//@Preview +@Composable +private fun DividerPreview() { + JetsnackTheme { + Box(Modifier.size(height = 10.dp, width = 100.dp)) { + JetsnackDivider(Modifier.align(Alignment.Center)) + } + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Filters.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Filters.kt new file mode 100644 index 0000000000..7e7ed982c9 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Filters.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.components + +import androidx.compose.animation.animateColorAsState +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp +import com.example.jetsnack.FilterList +import com.example.jetsnack.MppR +import com.example.jetsnack.label_filters +import com.example.jetsnack.model.Filter +import com.example.jetsnack.stringResource +import com.example.jetsnack.ui.theme.JetsnackTheme + +@Composable +fun FilterBar( + filters: List, + onShowFilters: () -> Unit +) { + + LazyRow( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(start = 12.dp, end = 8.dp), + modifier = Modifier.heightIn(min = 56.dp) + ) { + item { + IconButton(onClick = onShowFilters) { + Icon( + imageVector = Icons.Rounded.FilterList, + tint = JetsnackTheme.colors.brand, + contentDescription = stringResource(MppR.string.label_filters), + modifier = Modifier.diagonalGradientBorder( + colors = JetsnackTheme.colors.interactiveSecondary, + shape = CircleShape + ) + ) + } + } + items(filters) { filter -> + FilterChip(filter = filter, shape = MaterialTheme.shapes.small) + } + } +} + +@Composable +fun FilterChip( + filter: Filter, + modifier: Modifier = Modifier, + shape: Shape = MaterialTheme.shapes.small +) { + val (selected, setSelected) = filter.enabled + val backgroundColor by animateColorAsState( + if (selected) JetsnackTheme.colors.brandSecondary else JetsnackTheme.colors.uiBackground + ) + val border = Modifier.fadeInDiagonalGradientBorder( + showBorder = !selected, + colors = JetsnackTheme.colors.interactiveSecondary, + shape = shape + ) + val textColor by animateColorAsState( + if (selected) Color.Black else JetsnackTheme.colors.textSecondary + ) + + JetsnackSurface( + modifier = modifier.height(28.dp), + color = backgroundColor, + contentColor = textColor, + shape = shape, + elevation = 2.dp + ) { + val interactionSource = remember { MutableInteractionSource() } + + val pressed by interactionSource.collectIsPressedAsState() + val backgroundPressed = + if (pressed) { + Modifier.offsetGradientBackground( + JetsnackTheme.colors.interactiveSecondary, + 200f, + 0f + ) + } else { + Modifier.background(Color.Transparent) + } + Box( + modifier = Modifier + .toggleable( + value = selected, + onValueChange = setSelected, + interactionSource = interactionSource, + indication = null + ) + .then(backgroundPressed) + .then(border), + ) { + Text( + text = filter.name, + style = MaterialTheme.typography.caption, + maxLines = 1, + modifier = Modifier.padding( + horizontal = 20.dp, + vertical = 6.dp + ) + ) + } + } +} + +//@Preview +@Composable +private fun FilterDisabledPreview() { + JetsnackTheme { + FilterChip(Filter(name = "Demo", enabled = false), Modifier.padding(4.dp)) + } +} + +//@Preview +@Composable +private fun FilterEnabledPreview() { + JetsnackTheme { + FilterChip(Filter(name = "Demo", enabled = true)) + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Gradient.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Gradient.kt new file mode 100644 index 0000000000..ec26fe92f0 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Gradient.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.components + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +fun Modifier.diagonalGradientTint( + colors: List, + blendMode: BlendMode +) = drawWithContent { + drawContent() + drawRect( + brush = Brush.linearGradient(colors), + blendMode = blendMode + ) +} + +fun Modifier.offsetGradientBackground( + colors: List, + width: Float, + offset: Float = 0f +) = background( + Brush.horizontalGradient( + colors, + startX = -offset, + endX = width - offset, + tileMode = TileMode.Mirror + ) +) + +fun Modifier.diagonalGradientBorder( + colors: List, + borderSize: Dp = 2.dp, + shape: Shape +) = border( + width = borderSize, + brush = Brush.linearGradient(colors), + shape = shape +) + +fun Modifier.fadeInDiagonalGradientBorder( + showBorder: Boolean, + colors: List, + borderSize: Dp = 2.dp, + shape: Shape +) = composed { + val animatedColors = List(colors.size) { i -> + animateColorAsState(if (showBorder) colors[i] else colors[i].copy(alpha = 0f)).value + } + diagonalGradientBorder( + colors = animatedColors, + borderSize = borderSize, + shape = shape + ) +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/GradientTintedIconButton.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/GradientTintedIconButton.kt new file mode 100644 index 0000000000..ccafa8fe01 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/GradientTintedIconButton.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.components + +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.Surface +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import com.example.jetsnack.ui.theme.JetsnackTheme + +@Composable +fun JetsnackGradientTintedIconButton( + imageVector: ImageVector, + onClick: () -> Unit, + contentDescription: String?, + modifier: Modifier = Modifier, + colors: List = JetsnackTheme.colors.interactiveSecondary +) { + val interactionSource = remember { MutableInteractionSource() } + + // This should use a layer + srcIn but needs investigation + val border = Modifier.fadeInDiagonalGradientBorder( + showBorder = true, + colors = JetsnackTheme.colors.interactiveSecondary, + shape = CircleShape + ) + val pressed by interactionSource.collectIsPressedAsState() + val background = if (pressed) { + Modifier.offsetGradientBackground(colors, 200f, 0f) + } else { + Modifier.background(JetsnackTheme.colors.uiBackground) + } + val blendMode = if (JetsnackTheme.colors.isDark) BlendMode.Darken else BlendMode.Plus + val modifierColor = if (pressed) { + Modifier.diagonalGradientTint( + colors = listOf( + JetsnackTheme.colors.textSecondary, + JetsnackTheme.colors.textSecondary + ), + blendMode = blendMode + ) + } else { + Modifier.diagonalGradientTint( + colors = colors, + blendMode = blendMode + ) + } + Surface( + modifier = modifier + .clickable( + onClick = onClick, + interactionSource = interactionSource, + indication = null + ) + .clip(CircleShape) + .then(border) + .then(background), + color = Color.Transparent + ) { + Icon( + imageVector = imageVector, + contentDescription = contentDescription, + modifier = modifierColor + ) + } +} + +//@Preview +@Composable +private fun GradientTintedIconButtonPreview() { + JetsnackTheme { + JetsnackGradientTintedIconButton( + imageVector = Icons.Default.Add, + onClick = {}, + contentDescription = "Demo", + modifier = Modifier.padding(4.dp) + ) + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Grid.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Grid.kt new file mode 100644 index 0000000000..26d3151cc4 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Grid.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout + +/** + * A simple grid which lays elements out vertically in evenly sized [columns]. + */ +@Composable +fun VerticalGrid( + modifier: Modifier = Modifier, + columns: Int = 2, + content: @Composable () -> Unit +) { + Layout( + content = content, + modifier = modifier + ) { measurables, constraints -> + val itemWidth = constraints.maxWidth / columns + // Keep given height constraints, but set an exact width + val itemConstraints = constraints.copy( + minWidth = itemWidth, + maxWidth = itemWidth + ) + // Measure each item with these constraints + val placeables = measurables.map { it.measure(itemConstraints) } + // Track each columns height so we can calculate the overall height + val columnHeights = Array(columns) { 0 } + placeables.forEachIndexed { index, placeable -> + val column = index % columns + columnHeights[column] += placeable.height + } + val height = (columnHeights.maxOrNull() ?: constraints.minHeight) + .coerceAtMost(constraints.maxHeight) + layout( + width = constraints.maxWidth, + height = height + ) { + // Track the Y co-ord per column we have placed up to + val columnY = Array(columns) { 0 } + placeables.forEachIndexed { index, placeable -> + val column = index % columns + placeable.placeRelative( + x = column * itemWidth, + y = columnY[column] + ) + columnY[column] += placeable.height + } + } + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/QuantitySelector.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/QuantitySelector.kt new file mode 100644 index 0000000000..06fd7e435c --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/QuantitySelector.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.components + +import androidx.compose.animation.Crossfade +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.jetsnack.* +import com.example.jetsnack.ui.theme.JetsnackTheme + +@Composable +fun QuantitySelector( + count: Int, + decreaseItemCount: () -> Unit, + increaseItemCount: () -> Unit, + modifier: Modifier = Modifier +) { + Row(modifier = modifier) { + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Text( + text = stringResource(MppR.string.quantity), + style = MaterialTheme.typography.subtitle1, + color = JetsnackTheme.colors.textSecondary, + modifier = Modifier + .padding(end = 18.dp) + .align(Alignment.CenterVertically) + ) + } + JetsnackGradientTintedIconButton( + imageVector = Icons.Default.Remove, + onClick = decreaseItemCount, + contentDescription = stringResource(MppR.string.label_decrease), + modifier = Modifier.align(Alignment.CenterVertically) + ) + Crossfade( + targetState = count, + modifier = Modifier + .align(Alignment.CenterVertically) + ) { + Text( + text = "$it", + style = MaterialTheme.typography.subtitle2, + fontSize = 18.sp, + color = JetsnackTheme.colors.textPrimary, + textAlign = TextAlign.Center, + modifier = Modifier.widthIn(min = 24.dp) + ) + } + JetsnackGradientTintedIconButton( + imageVector = Icons.Default.Add, + onClick = increaseItemCount, + contentDescription = stringResource(MppR.string.label_increase), + modifier = Modifier.align(Alignment.CenterVertically) + ) + } +} + +//@Preview +@Composable +fun QuantitySelectorPreview() { + JetsnackTheme { + JetsnackSurface { + QuantitySelector(1, {}, {}) + } + } +} + +//@Preview +@Composable +fun QuantitySelectorPreviewRtl() { + JetsnackTheme { + JetsnackSurface { + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + QuantitySelector(1, {}, {}) + } + } + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Scaffold.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Scaffold.kt new file mode 100644 index 0000000000..337a02d8c6 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Scaffold.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.components + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material.DrawerDefaults +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.FabPosition +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.ScaffoldState +import androidx.compose.material.SnackbarHost +import androidx.compose.material.SnackbarHostState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import com.example.jetsnack.ui.theme.JetsnackTheme + +/** + * Wrap Material [androidx.compose.material.Scaffold] and set [JetsnackTheme] colors. + */ +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun JetsnackScaffold( + modifier: Modifier = Modifier, + scaffoldState: ScaffoldState = rememberScaffoldState(), + topBar: @Composable (() -> Unit) = {}, + bottomBar: @Composable (() -> Unit) = {}, + snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, + floatingActionButton: @Composable (() -> Unit) = {}, + floatingActionButtonPosition: FabPosition = FabPosition.End, + isFloatingActionButtonDocked: Boolean = false, + drawerContent: @Composable (ColumnScope.() -> Unit)? = null, + drawerShape: Shape = MaterialTheme.shapes.large, + drawerElevation: Dp = DrawerDefaults.Elevation, + drawerBackgroundColor: Color = JetsnackTheme.colors.uiBackground, + drawerContentColor: Color = JetsnackTheme.colors.textSecondary, + drawerScrimColor: Color = JetsnackTheme.colors.uiBorder, + backgroundColor: Color = JetsnackTheme.colors.uiBackground, + contentColor: Color = JetsnackTheme.colors.textSecondary, + content: @Composable (PaddingValues) -> Unit +) { + Scaffold( + modifier = modifier, + scaffoldState = scaffoldState, + topBar = topBar, + bottomBar = bottomBar, + snackbarHost = snackbarHost, + floatingActionButton = floatingActionButton, + floatingActionButtonPosition = floatingActionButtonPosition, + isFloatingActionButtonDocked = isFloatingActionButtonDocked, + drawerContent = drawerContent, + drawerShape = drawerShape, + drawerElevation = drawerElevation, + drawerBackgroundColor = drawerBackgroundColor, + drawerContentColor = drawerContentColor, + drawerScrimColor = drawerScrimColor, + backgroundColor = backgroundColor, + contentColor = contentColor, + content = content + ) +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Snackbar.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Snackbar.kt new file mode 100644 index 0000000000..0c745c2875 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Snackbar.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.components + +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Snackbar +import androidx.compose.material.SnackbarData +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.example.jetsnack.ui.theme.JetsnackTheme + +/** + * An alternative to [androidx.compose.material.Snackbar] utilizing + * [com.example.jetsnack.ui.theme.JetsnackColors] + */ +@Composable +fun JetsnackSnackbar( + snackbarData: SnackbarData, + modifier: Modifier = Modifier, + actionOnNewLine: Boolean = false, + shape: Shape = MaterialTheme.shapes.small, + backgroundColor: Color = JetsnackTheme.colors.uiBackground, + contentColor: Color = JetsnackTheme.colors.textSecondary, + actionColor: Color = JetsnackTheme.colors.brand, + elevation: Dp = 6.dp +) { + Snackbar( + snackbarData = snackbarData, + modifier = modifier, + actionOnNewLine = actionOnNewLine, + shape = shape, + backgroundColor = backgroundColor, + contentColor = contentColor, + actionColor = actionColor, + elevation = elevation + ) +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Snacks.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Snacks.kt new file mode 100644 index 0000000000..5842fd4e1e --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Snacks.kt @@ -0,0 +1,308 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.components + +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material.icons.outlined.ArrowForward +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.example.jetsnack.model.CollectionType +import com.example.jetsnack.model.Snack +import com.example.jetsnack.model.SnackCollection +import com.example.jetsnack.model.snacks +import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.utils.mirroringIcon + +private val HighlightCardWidth = 170.dp +private val HighlightCardPadding = 16.dp + +// The Cards show a gradient which spans 3 cards and scrolls with parallax. +private val gradientWidth + @Composable + get() = with(LocalDensity.current) { + (3 * (HighlightCardWidth + HighlightCardPadding).toPx()) + } + +@Composable +fun SnackCollection( + snackCollection: SnackCollection, + onSnackClick: (Long) -> Unit, + modifier: Modifier = Modifier, + index: Int = 0, + highlight: Boolean = true +) { + Column(modifier = modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .heightIn(min = 56.dp) + .padding(start = 24.dp) + ) { + Text( + text = snackCollection.name, + style = MaterialTheme.typography.h6, + color = JetsnackTheme.colors.brand, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(1f) + .wrapContentWidth(Alignment.Start) + ) + IconButton( + onClick = { /* todo */ }, + modifier = Modifier.align(Alignment.CenterVertically) + ) { + Icon( + imageVector = mirroringIcon( + ltrIcon = Icons.Outlined.ArrowForward, + rtlIcon = Icons.Outlined.ArrowBack + ), + tint = JetsnackTheme.colors.brand, + contentDescription = null + ) + } + } + if (highlight && snackCollection.type == CollectionType.Highlight) { + HighlightedSnacks(index, snackCollection.snacks, onSnackClick) + } else { + Snacks(snackCollection.snacks, onSnackClick) + } + } +} + +@Composable +private fun HighlightedSnacks( + index: Int, + snacks: List, + onSnackClick: (Long) -> Unit, + modifier: Modifier = Modifier +) { + val scroll = rememberScrollState(0) + val gradient = when ((index / 2) % 2) { + 0 -> JetsnackTheme.colors.gradient6_1 + else -> JetsnackTheme.colors.gradient6_2 + } + // The Cards show a gradient which spans 3 cards and scrolls with parallax. + val gradientWidth = with(LocalDensity.current) { + (6 * (HighlightCardWidth + HighlightCardPadding).toPx()) + } + LazyRow( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(start = 24.dp, end = 24.dp) + ) { + itemsIndexed(snacks) { index, snack -> + HighlightSnackItem( + snack, + onSnackClick, + index, + gradient, + gradientWidth, + scroll.value + ) + } + } +} + +@Composable +private fun Snacks( + snacks: List, + onSnackClick: (Long) -> Unit, + modifier: Modifier = Modifier +) { + LazyRow( + modifier = modifier, + contentPadding = PaddingValues(start = 12.dp, end = 12.dp) + ) { + items(snacks) { snack -> + SnackItem(snack, onSnackClick) + } + } +} + +@Composable +fun SnackItem( + snack: Snack, + onSnackClick: (Long) -> Unit, + modifier: Modifier = Modifier +) { + JetsnackSurface( + shape = MaterialTheme.shapes.medium, + modifier = modifier.padding( + start = 4.dp, + end = 4.dp, + bottom = 8.dp + ) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .clickable(onClick = { onSnackClick(snack.id) }) + .padding(8.dp) + ) { + SnackImage( + imageUrl = snack.imageUrl, + elevation = 4.dp, + contentDescription = null, + modifier = Modifier.size(120.dp) + ) + Text( + text = snack.name, + style = MaterialTheme.typography.subtitle1, + color = JetsnackTheme.colors.textSecondary, + modifier = Modifier.padding(top = 8.dp) + ) + } + } +} + +@Composable +private fun HighlightSnackItem( + snack: Snack, + onSnackClick: (Long) -> Unit, + index: Int, + gradient: List, + gradientWidth: Float, + scroll: Int, + modifier: Modifier = Modifier +) { + val left = index * with(LocalDensity.current) { + (HighlightCardWidth + HighlightCardPadding).toPx() + } + JetsnackCard( + modifier = modifier + .size( + width = 170.dp, + height = 250.dp + ) + .padding(bottom = 16.dp) + ) { + Column( + modifier = Modifier + .clickable(onClick = { onSnackClick(snack.id) }) + .fillMaxSize() + ) { + Box( + modifier = Modifier + .height(160.dp) + .fillMaxWidth() + ) { + val gradientOffset = left - (scroll / 3f) + Box( + modifier = Modifier + .height(100.dp) + .fillMaxWidth() + .offsetGradientBackground(gradient, gradientWidth, gradientOffset) + ) + SnackImage( + imageUrl = snack.imageUrl, + contentDescription = null, + modifier = Modifier + .size(120.dp) + .align(Alignment.BottomCenter) + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = snack.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.h6, + color = JetsnackTheme.colors.textSecondary, + modifier = Modifier.padding(horizontal = 16.dp) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = snack.tagline, + style = MaterialTheme.typography.body1, + color = JetsnackTheme.colors.textHelp, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } +} + +@Composable +fun SnackImage( + imageUrl: String, + contentDescription: String?, + modifier: Modifier = Modifier, + elevation: Dp = 0.dp +) { + JetsnackSurface( + color = Color.LightGray, + elevation = elevation, + shape = CircleShape, + modifier = modifier + ) { + SnackAsyncImage(imageUrl, contentDescription, Modifier.fillMaxSize()) + } +} + +@Composable +expect fun SnackAsyncImage( + imageUrl: String, + contentDescription: String?, + modifier: Modifier +) + +//@Preview +@Composable +fun SnackCardPreview() { + JetsnackTheme { + val snack = snacks.first() + HighlightSnackItem( + snack = snack, + onSnackClick = { }, + index = 0, + gradient = JetsnackTheme.colors.gradient6_1, + gradientWidth = gradientWidth, + scroll = 0 + ) + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Surface.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Surface.kt new file mode 100644 index 0000000000..584c446367 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/components/Surface.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.material.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import com.example.jetsnack.ui.theme.JetsnackTheme +import kotlin.math.ln + +/** + * An alternative to [androidx.compose.material.Surface] utilizing + * [com.example.jetsnack.ui.theme.JetsnackColors] + */ +@Composable +fun JetsnackSurface( + modifier: Modifier = Modifier, + shape: Shape = RectangleShape, + color: Color = JetsnackTheme.colors.uiBackground, + contentColor: Color = JetsnackTheme.colors.textSecondary, + border: BorderStroke? = null, + elevation: Dp = 0.dp, + content: @Composable () -> Unit +) { + Box( + modifier = modifier.shadow(elevation = elevation, shape = shape, clip = false) + .zIndex(elevation.value) + .then(if (border != null) Modifier.border(border, shape) else Modifier) + .background( + color = getBackgroundColorForElevation(color, elevation), + shape = shape + ) + .clip(shape) + ) { + CompositionLocalProvider(LocalContentColor provides contentColor, content = content) + } +} + +@Composable +private fun getBackgroundColorForElevation(color: Color, elevation: Dp): Color { + return if (elevation > 0.dp // && https://issuetracker.google.com/issues/161429530 + // JetsnackTheme.colors.isDark //&& + // color == JetsnackTheme.colors.uiBackground + ) { + color.withElevation(elevation) + } else { + color + } +} + +/** + * Applies a [Color.White] overlay to this color based on the [elevation]. This increases visibility + * of elevation for surfaces in a dark theme. + * + * TODO: Remove when public https://issuetracker.google.com/155181601 + */ +private fun Color.withElevation(elevation: Dp): Color { + val foreground = calculateForeground(elevation) + return foreground.compositeOver(this) +} + +/** + * @return the alpha-modified [Color.White] to overlay on top of the surface color to produce + * the resultant color. + */ +private fun calculateForeground(elevation: Dp): Color { + val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f + return Color.White.copy(alpha = alpha) +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/DestinationBar.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/DestinationBar.kt new file mode 100644 index 0000000000..b42560861d --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/DestinationBar.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.home + +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Column +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.example.jetsnack.ExpandMore +import com.example.jetsnack.MppR +import com.example.jetsnack.label_select_delivery +import com.example.jetsnack.stringResource +import com.example.jetsnack.ui.components.JetsnackDivider +import com.example.jetsnack.ui.snackdetail.jetSnackStatusBarsPadding +import com.example.jetsnack.ui.theme.AlphaNearOpaque +import com.example.jetsnack.ui.theme.JetsnackTheme + +@Composable +fun DestinationBar(modifier: Modifier = Modifier) { + Column(modifier = modifier.jetSnackStatusBarsPadding()) { + TopAppBar( + backgroundColor = JetsnackTheme.colors.uiBackground.copy(alpha = AlphaNearOpaque), + contentColor = JetsnackTheme.colors.textSecondary, + elevation = 0.dp + ) { + Text( + text = "Huidekoperstraat 26-28, 1017 ZM Amsterdam | https://kotl.in/wasm-gio23", + style = MaterialTheme.typography.subtitle1, + color = JetsnackTheme.colors.textSecondary, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically) + ) + IconButton( + onClick = { /* todo */ }, + modifier = Modifier.align(Alignment.CenterVertically) + ) { + Icon( + imageVector = Icons.Outlined.ExpandMore, + tint = JetsnackTheme.colors.brand, + contentDescription = stringResource(MppR.string.label_select_delivery) + ) + } + } + JetsnackDivider() + } +} + +//@Preview +@Composable +fun PreviewDestinationBar() { + JetsnackTheme { + DestinationBar() + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Feed.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Feed.kt new file mode 100644 index 0000000000..14db005d06 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Feed.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.home + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.windowInsetsTopHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.example.jetsnack.model.Filter +import com.example.jetsnack.model.SnackCollection +import com.example.jetsnack.model.SnackRepo +import com.example.jetsnack.ui.components.FilterBar +import com.example.jetsnack.ui.components.JetsnackDivider +import com.example.jetsnack.ui.components.JetsnackSurface +import com.example.jetsnack.ui.components.SnackCollection +import com.example.jetsnack.ui.theme.JetsnackTheme + +@Composable +fun Feed( + onSnackClick: (Long) -> Unit, + modifier: Modifier = Modifier +) { + val snackCollections = remember { SnackRepo.getSnacks() } + val filters = remember { SnackRepo.getFilters() } + Feed( + snackCollections, + filters, + onSnackClick, + modifier + ) +} + +@Composable +private fun Feed( + snackCollections: List, + filters: List, + onSnackClick: (Long) -> Unit, + modifier: Modifier = Modifier +) { + + JetsnackSurface(modifier = modifier.fillMaxSize()) { + Box { + SnackCollectionList(snackCollections, filters, onSnackClick) + DestinationBar() + } + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun SnackCollectionList( + snackCollections: List, + filters: List, + onSnackClick: (Long) -> Unit, + modifier: Modifier = Modifier +) { + var filtersVisible by rememberSaveable { mutableStateOf(false) } + Box(modifier) { + LazyColumn { + + item { + Spacer( + Modifier.windowInsetsTopHeight(snackCollectionListItemWindowInsets()) + ) + FilterBar(filters, onShowFilters = { filtersVisible = true }) + } + itemsIndexed(snackCollections) { index, snackCollection -> + if (index > 0) { + JetsnackDivider(thickness = 2.dp) + } + + SnackCollection( + snackCollection = snackCollection, + onSnackClick = onSnackClick, + index = index + ) + } + } + } + AnimatedVisibility( + visible = filtersVisible, + enter = slideInVertically() + expandVertically( + expandFrom = Alignment.Top + ) + fadeIn(initialAlpha = 0.3f), + exit = slideOutVertically() + shrinkVertically() + fadeOut() + ) { + FilterScreen( + onDismiss = { filtersVisible = false } + ) + } +} + +@Composable +expect fun snackCollectionListItemWindowInsets(): WindowInsets + +//@Preview +@Composable +fun HomePreview() { + JetsnackTheme { + Feed(onSnackClick = { }) + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/FilterScreen.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/FilterScreen.kt new file mode 100644 index 0000000000..1c22e86f92 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/FilterScreen.kt @@ -0,0 +1,275 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.home + +import androidx.compose.animation.ExperimentalAnimationApi +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ContentAlpha +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Slider +import androidx.compose.material.SliderDefaults +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Done +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.example.jetsnack.* +import com.example.jetsnack.flowlayout.FlowMainAxisAlignment +import com.example.jetsnack.flowlayout.FlowRow +import com.example.jetsnack.model.Filter +import com.example.jetsnack.model.SnackRepo +import com.example.jetsnack.ui.components.FilterChip +import com.example.jetsnack.ui.components.JetsnackScaffold +import com.example.jetsnack.ui.theme.JetsnackTheme + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun FilterScreen( + onDismiss: () -> Unit +) { + var sortState by remember { mutableStateOf(SnackRepo.getSortDefault()) } + var maxCalories by remember { mutableStateOf(0f) } + val defaultFilter = SnackRepo.getSortDefault() + + SnackDialog(onCloseRequest = onDismiss) { + val priceFilters = remember { SnackRepo.getPriceFilters() } + val categoryFilters = remember { SnackRepo.getCategoryFilters() } + val lifeStyleFilters = remember { SnackRepo.getLifeStyleFilters() } + JetsnackScaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = onDismiss) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(id = MppR.string.close) + ) + } + }, + title = { + Text( + text = stringResource(id = MppR.string.label_filters), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.h6 + ) + }, + actions = { + var resetEnabled = sortState != defaultFilter + IconButton( + onClick = { /* TODO: Open search */ }, + enabled = resetEnabled + ) { + val alpha = if (resetEnabled) { + ContentAlpha.high + } else { + ContentAlpha.disabled + } + CompositionLocalProvider(LocalContentAlpha provides alpha) { + Text( + text = stringResource(id = MppR.string.reset), + style = MaterialTheme.typography.body2 + ) + } + } + }, + backgroundColor = JetsnackTheme.colors.uiBackground + ) + } + ) { + Column( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp, vertical = 16.dp), + ) { + SortFiltersSection( + sortState = sortState, + onFilterChange = { filter -> + sortState = filter.name + } + ) + FilterChipSection( + title = stringResource(id = MppR.string.price), + filters = priceFilters + ) + FilterChipSection( + title = stringResource(id = MppR.string.category), + filters = categoryFilters + ) + + MaxCalories( + sliderPosition = maxCalories, + onValueChanged = { newValue -> + maxCalories = newValue + } + ) + FilterChipSection( + title = stringResource(id = MppR.string.lifestyle), + filters = lifeStyleFilters + ) + } + } + } +} + +@Composable +expect fun SnackDialog(onCloseRequest: () -> Unit, content: @Composable () -> Unit) + + +@Composable +fun FilterChipSection(title: String, filters: List) { + FilterTitle(text = title) + FlowRow( + mainAxisAlignment = FlowMainAxisAlignment.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 16.dp) + .padding(horizontal = 4.dp) + ) { + filters.forEach { filter -> + FilterChip( + filter = filter, + modifier = Modifier.padding(end = 4.dp, bottom = 8.dp) + ) + } + } +} + +@Composable +fun SortFiltersSection(sortState: String, onFilterChange: (Filter) -> Unit) { + FilterTitle(text = stringResource(id = MppR.string.sort)) + Column(Modifier.padding(bottom = 24.dp)) { + SortFilters( + sortState = sortState, + onChanged = onFilterChange + ) + } +} + +@Composable +fun SortFilters( + sortFilters: List = SnackRepo.getSortFilters(), + sortState: String, + onChanged: (Filter) -> Unit +) { + + sortFilters.forEach { filter -> + SortOption( + text = filter.name, + icon = filter.icon, + selected = sortState == filter.name, + onClickOption = { + onChanged(filter) + } + ) + } +} + +@Composable +fun MaxCalories(sliderPosition: Float, onValueChanged: (Float) -> Unit) { + FlowRow { + FilterTitle(text = stringResource(id = MppR.string.max_calories)) + Text( + text = stringResource(id = MppR.string.per_serving), + style = MaterialTheme.typography.body2, + color = JetsnackTheme.colors.brand, + modifier = Modifier.padding(top = 5.dp, start = 10.dp) + ) + } + Slider( + value = sliderPosition, + onValueChange = { newValue -> + onValueChanged(newValue) + }, + valueRange = 0f..300f, + steps = 5, + modifier = Modifier + .fillMaxWidth(), + colors = SliderDefaults.colors( + thumbColor = JetsnackTheme.colors.brand, + activeTrackColor = JetsnackTheme.colors.brand + ) + ) +} + +@Composable +fun FilterTitle(text: String) { + Text( + text = text, + style = MaterialTheme.typography.h6, + color = JetsnackTheme.colors.brand, + modifier = Modifier.padding(bottom = 8.dp) + ) +} +@Composable +fun SortOption( + text: String, + icon: ImageVector?, + onClickOption: () -> Unit, + selected: Boolean +) { + Row( + modifier = Modifier + .padding(top = 14.dp) + .selectable(selected) { onClickOption() } + ) { + if (icon != null) { + Icon(imageVector = icon, contentDescription = null) + } + Text( + text = text, + style = MaterialTheme.typography.subtitle1, + modifier = Modifier + .padding(start = 10.dp) + .weight(1f) + ) + if (selected) { + Icon( + imageVector = Icons.Filled.Done, + contentDescription = null, + tint = JetsnackTheme.colors.brand + ) + } + } +} +//@Preview +@Composable +fun FilterScreenPreview() { + FilterScreen(onDismiss = {}) +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Home.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Home.kt new file mode 100644 index 0000000000..98563b440a --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Home.kt @@ -0,0 +1,363 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.home + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.SpringSpec +import androidx.compose.animation.core.animateFloatAsState +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AccountCircle +import androidx.compose.material.icons.outlined.Home +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material.icons.outlined.ShoppingCart +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.example.jetsnack.* +import com.example.jetsnack.ui.components.JetsnackSurface +import com.example.jetsnack.ui.snackdetail.jetSnackNavigationBarsPadding +import com.example.jetsnack.ui.snackdetail.lerp +import com.example.jetsnack.ui.theme.JetsnackTheme + +//fun NavGraphBuilder.addHomeGraph( +// onSnackSelected: (Long, NavBackStackEntry) -> Unit, +// modifier: Modifier = Modifier +//) { +// composable(HomeSections.FEED.route) { from -> +// Feed(onSnackClick = { id -> onSnackSelected(id, from) }, modifier) +// } +// composable(HomeSections.SEARCH.route) { from -> +// Search(onSnackClick = { id -> onSnackSelected(id, from) }, modifier) +// } +// composable(HomeSections.CART.route) { from -> +// Cart(onSnackClick = { id -> onSnackSelected(id, from) }, modifier) +// } +// composable(HomeSections.PROFILE.route) { +// Profile(modifier) +// } +//} + +enum class HomeSections( + val title: Int, // @StringRes + val icon: ImageVector, + val route: String +) { + FEED(MppR.string.home_feed, Icons.Outlined.Home, "home/feed"), + SEARCH(MppR.string.home_search, Icons.Outlined.Search, "home/search"), + CART(MppR.string.home_cart, Icons.Outlined.ShoppingCart, "home/cart"), + PROFILE(MppR.string.home_profile, Icons.Outlined.AccountCircle, "home/profile") +} + +@Composable +fun JetsnackBottomBar( + tabs: Array, + currentRoute: String, + navigateToRoute: (String) -> Unit, + color: Color = JetsnackTheme.colors.iconPrimary, + contentColor: Color = JetsnackTheme.colors.iconInteractive +) { + val routes = remember { tabs.map { it.route } } + val currentSection = tabs.first { it.route == currentRoute } + + JetsnackSurface( + color = color, + contentColor = contentColor + ) { + val springSpec = SpringSpec( + // Determined experimentally + stiffness = 800f, + dampingRatio = 0.8f + ) + JetsnackBottomNavLayout( + selectedIndex = currentSection.ordinal, + itemCount = routes.size, + indicator = { JetsnackBottomNavIndicator() }, + animSpec = springSpec, + modifier = Modifier.jetSnackNavigationBarsPadding() + ) { + // TODO: implement getting currentLocale in common source set +// val configuration = LocalConfiguration.current +// val currentLocale: Locale = +// ConfigurationCompat.getLocales(configuration).get(0) ?: Locale.getDefault() + + tabs.forEach { section -> + val selected = section == currentSection + val tint by animateColorAsState( + if (selected) { + JetsnackTheme.colors.iconInteractive + } else { + JetsnackTheme.colors.iconInteractiveInactive + } + ) + + // TODO: implement uppercase using currentLocale +// val text = stringResource(section.title).uppercase(currentLocale) + val text = stringResource(section.title).uppercase() + + JetsnackBottomNavigationItem( + icon = { + Icon( + imageVector = section.icon, + tint = tint, + contentDescription = text + ) + }, + text = { + Text( + text = text, + color = tint, + style = MaterialTheme.typography.button, + maxLines = 1 + ) + }, + selected = selected, + onSelected = { navigateToRoute(section.route) }, + animSpec = springSpec, + modifier = BottomNavigationItemPadding + .clip(BottomNavIndicatorShape) + ) + } + } + } +} + +@Composable +private fun JetsnackBottomNavLayout( + selectedIndex: Int, + itemCount: Int, + animSpec: AnimationSpec, + indicator: @Composable BoxScope.() -> Unit, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + // Track how "selected" each item is [0, 1] + val selectionFractions = remember(itemCount) { + List(itemCount) { i -> + Animatable(if (i == selectedIndex) 1f else 0f) + } + } + selectionFractions.forEachIndexed { index, selectionFraction -> + val target = if (index == selectedIndex) 1f else 0f + LaunchedEffect(target, animSpec) { + selectionFraction.animateTo(target, animSpec) + } + } + + // Animate the position of the indicator + val indicatorIndex = remember { Animatable(0f) } + val targetIndicatorIndex = selectedIndex.toFloat() + LaunchedEffect(targetIndicatorIndex) { + indicatorIndex.animateTo(targetIndicatorIndex, animSpec) + } + + Layout( + modifier = modifier.height(BottomNavHeight), + content = { + content() + Box(Modifier.layoutId("indicator"), content = indicator) + } + ) { measurables, constraints -> + check(itemCount == (measurables.size - 1)) // account for indicator + + // Divide the width into n+1 slots and give the selected item 2 slots + val unselectedWidth = constraints.maxWidth / (itemCount + 1) + val selectedWidth = 2 * unselectedWidth + val indicatorMeasurable = measurables.first { it.layoutId == "indicator" } + + val itemPlaceables = measurables + .filterNot { it == indicatorMeasurable } + .mapIndexed { index, measurable -> + // Animate item's width based upon the selection amount + val width = lerp(unselectedWidth, selectedWidth, selectionFractions[index].value) + measurable.measure( + constraints.copy( + minWidth = width, + maxWidth = width + ) + ) + } + val indicatorPlaceable = indicatorMeasurable.measure( + constraints.copy( + minWidth = selectedWidth, + maxWidth = selectedWidth + ) + ) + + layout( + width = constraints.maxWidth, + height = itemPlaceables.maxByOrNull { it.height }?.height ?: 0 + ) { + val indicatorLeft = indicatorIndex.value * unselectedWidth + indicatorPlaceable.placeRelative(x = indicatorLeft.toInt(), y = 0) + var x = 0 + itemPlaceables.forEach { placeable -> + placeable.placeRelative(x = x, y = 0) + x += placeable.width + } + } + } +} + +@Composable +fun JetsnackBottomNavigationItem( + icon: @Composable BoxScope.() -> Unit, + text: @Composable BoxScope.() -> Unit, + selected: Boolean, + onSelected: () -> Unit, + animSpec: AnimationSpec, + modifier: Modifier = Modifier +) { + // Animate the icon/text positions within the item based on selection + val animationProgress by animateFloatAsState(if (selected) 1f else 0f, animSpec) + JetsnackBottomNavItemLayout( + icon = icon, + text = text, + animationProgress = animationProgress, + modifier = modifier + .selectable(selected = selected, onClick = onSelected) + .wrapContentSize() + ) +} + +@Composable +private fun JetsnackBottomNavItemLayout( + icon: @Composable BoxScope.() -> Unit, + text: @Composable BoxScope.() -> Unit, + animationProgress: Float, // @FloatRange(from = 0.0, to = 1.0) + modifier: Modifier = Modifier +) { + Layout( + modifier = modifier, + content = { + Box( + modifier = Modifier + .layoutId("icon") + .padding(horizontal = TextIconSpacing), + content = icon + ) + val scale = lerp(0.6f, 1f, animationProgress) + Box( + modifier = Modifier + .layoutId("text") + .padding(horizontal = TextIconSpacing) + .graphicsLayer { + alpha = animationProgress + scaleX = scale + scaleY = scale + transformOrigin = BottomNavLabelTransformOrigin + }, + content = text + ) + } + ) { measurables, constraints -> + val iconPlaceable = measurables.first { it.layoutId == "icon" }.measure(constraints) + val textPlaceable = measurables.first { it.layoutId == "text" }.measure(constraints) + + placeTextAndIcon( + textPlaceable, + iconPlaceable, + constraints.maxWidth, + constraints.maxHeight, + animationProgress + ) + } +} + +private fun MeasureScope.placeTextAndIcon( + textPlaceable: Placeable, + iconPlaceable: Placeable, + width: Int, + height: Int, + animationProgress: Float // @FloatRange(from = 0.0, to = 1.0) +): MeasureResult { + val iconY = (height - iconPlaceable.height) / 2 + val textY = (height - textPlaceable.height) / 2 + + val textWidth = textPlaceable.width * animationProgress + val iconX = (width - textWidth - iconPlaceable.width) / 2 + val textX = iconX + iconPlaceable.width + + return layout(width, height) { + iconPlaceable.placeRelative(iconX.toInt(), iconY) + if (animationProgress != 0f) { + textPlaceable.placeRelative(textX.toInt(), textY) + } + } +} + +@Composable +private fun JetsnackBottomNavIndicator( + strokeWidth: Dp = 2.dp, + color: Color = JetsnackTheme.colors.iconInteractive, + shape: Shape = BottomNavIndicatorShape +) { + Spacer( + modifier = Modifier + .fillMaxSize() + .then(BottomNavigationItemPadding) + .border(strokeWidth, color, shape) + ) +} + +private val TextIconSpacing = 2.dp +private val BottomNavHeight = 56.dp +private val BottomNavLabelTransformOrigin = TransformOrigin(0f, 0.5f) +private val BottomNavIndicatorShape = RoundedCornerShape(percent = 50) +private val BottomNavigationItemPadding = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + +//@Preview +@Composable +private fun JetsnackBottomNavPreview() { + JetsnackTheme { + JetsnackBottomBar( + tabs = HomeSections.values(), + currentRoute = "home/feed", + navigateToRoute = { } + ) + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Profile.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Profile.kt new file mode 100644 index 0000000000..c55b10275d --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/Profile.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.home + +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.example.jetsnack.* +import com.example.jetsnack.ui.theme.JetsnackTheme + +@Composable +fun Profile(modifier: Modifier = Modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxSize() + .wrapContentSize() + .padding(24.dp) + ) { + Image( + painterResource(MppR.drawable.empty_state_search), + contentDescription = null + ) + Spacer(Modifier.height(24.dp)) + Text( + text = stringResource(MppR.string.work_in_progress), + style = MaterialTheme.typography.subtitle1, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(16.dp)) + Text( + text = stringResource(MppR.string.grab_beverage), + style = MaterialTheme.typography.body2, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } +} + +//@Preview +@Composable +fun ProfilePreview() { + JetsnackTheme { + Profile() + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.kt new file mode 100644 index 0000000000..6f5f694eb4 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.kt @@ -0,0 +1,343 @@ +package com.example.jetsnack.ui.home.cart + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.LastBaseline +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.example.jetsnack.* +import com.example.jetsnack.model.OrderLine +import com.example.jetsnack.model.SnackCollection +import com.example.jetsnack.model.SnackRepo +import com.example.jetsnack.ui.components.JetsnackButton +import com.example.jetsnack.ui.components.JetsnackDivider +import com.example.jetsnack.ui.components.JetsnackSurface +import com.example.jetsnack.ui.components.SnackCollection +import com.example.jetsnack.ui.home.DestinationBar +import com.example.jetsnack.ui.theme.AlphaNearOpaque +import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.utils.formatPrice + + +@Composable +fun Cart( + onSnackClick: (Long) -> Unit, + modifier: Modifier = Modifier, + viewModel: CartViewModel = provideCartViewModel() +) { + val orderLines by viewModel.collectOrderLinesAsState(viewModel.orderLines) + val inspiredByCart = remember { SnackRepo.getInspiredByCart() } + Cart( + orderLines = orderLines, + removeSnack = viewModel::removeSnack, + increaseItemCount = viewModel::increaseSnackCount, + decreaseItemCount = viewModel::decreaseSnackCount, + inspiredByCart = inspiredByCart, + onSnackClick = onSnackClick, + modifier = modifier + ) +} + +@Composable +expect fun provideCartViewModel(): CartViewModel + +/** + * Android uses ConstraintLayout which is android-only at the moment. + * So we provide an alternative implementation of `ActualCartItem` for other platforms. + */ +@Composable +expect fun ActualCartItem( + orderLine: OrderLine, + removeSnack: (Long) -> Unit, + increaseItemCount: (Long) -> Unit, + decreaseItemCount: (Long) -> Unit, + onSnackClick: (Long) -> Unit, + modifier: Modifier = Modifier +) + +@Composable +fun Cart( + orderLines: List, + removeSnack: (Long) -> Unit, + increaseItemCount: (Long) -> Unit, + decreaseItemCount: (Long) -> Unit, + inspiredByCart: SnackCollection, + onSnackClick: (Long) -> Unit, + modifier: Modifier = Modifier +) { + JetsnackSurface(modifier = modifier.fillMaxSize()) { + Box { + CartContent( + orderLines = orderLines, + removeSnack = removeSnack, + increaseItemCount = increaseItemCount, + decreaseItemCount = decreaseItemCount, + inspiredByCart = inspiredByCart, + onSnackClick = onSnackClick, + modifier = Modifier.align(Alignment.TopCenter) + ) + DestinationBar(modifier = Modifier.align(Alignment.TopCenter)) + CheckoutBar(modifier = Modifier.align(Alignment.BottomCenter)) + } + } +} + +@Composable +expect fun rememberQuantityString(res: Int, qty: Int, vararg args: Any): String + +@Composable +expect fun getCartContentInsets(): WindowInsets + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun CartContent( + orderLines: List, + removeSnack: (Long) -> Unit, + increaseItemCount: (Long) -> Unit, + decreaseItemCount: (Long) -> Unit, + inspiredByCart: SnackCollection, + onSnackClick: (Long) -> Unit, + modifier: Modifier = Modifier +) { + val snackCountFormattedString = rememberQuantityString( + MppR.plurals.cart_order_count, orderLines.size, orderLines.size + ) + LazyColumn(modifier) { + item { + Spacer(Modifier.windowInsetsTopHeight(getCartContentInsets())) + Text( + text = stringResource(MppR.string.cart_order_header, snackCountFormattedString), + style = MaterialTheme.typography.h6, + color = JetsnackTheme.colors.brand, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .heightIn(min = 56.dp) + .padding(horizontal = 24.dp, vertical = 4.dp) + .wrapContentHeight() + ) + } + items(orderLines) { orderLine -> + SwipeDismissItem( + background = { offsetX -> + /*Background color changes from light gray to red when the + swipe to delete with exceeds 160.dp*/ + val backgroundColor = if (offsetX < -160.dp) { + JetsnackTheme.colors.error + } else { + JetsnackTheme.colors.uiFloated + } + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .background(backgroundColor), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.Center + ) { + // Set 4.dp padding only if offset is bigger than 160.dp + val padding: Dp by animateDpAsState( + if (offsetX > -160.dp) 4.dp else 0.dp + ) + Box( + Modifier + .width(offsetX * -1) + .padding(padding) + ) { + // Height equals to width removing padding + val height = (offsetX + 8.dp) * -1 + Surface( + modifier = Modifier + .fillMaxWidth() + .height(height) + .align(Alignment.Center), + shape = CircleShape, + color = JetsnackTheme.colors.error + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + // Icon must be visible while in this width range + if (offsetX < -40.dp && offsetX > -152.dp) { + // Icon alpha decreases as it is about to disappear + val iconAlpha: Float by animateFloatAsState( + if (offsetX < -120.dp) 0.5f else 1f + ) + + Icon( + imageVector = Icons.Filled.DeleteForever, + modifier = Modifier + .size(16.dp) + .graphicsLayer(alpha = iconAlpha), + tint = JetsnackTheme.colors.uiBackground, + contentDescription = null, + ) + } + /*Text opacity increases as the text is supposed to appear in + the screen*/ + val textAlpha by animateFloatAsState( + if (offsetX > -144.dp) 0.5f else 1f + ) + if (offsetX < -120.dp) { + Text( + text = stringResource(id = MppR.string.remove_item), + style = MaterialTheme.typography.subtitle1, + color = JetsnackTheme.colors.uiBackground, + textAlign = TextAlign.Center, + modifier = Modifier + .graphicsLayer( + alpha = textAlpha + ) + ) + } + } + } + } + } + }, + ) { + ActualCartItem( + orderLine = orderLine, + removeSnack = removeSnack, + increaseItemCount = increaseItemCount, + decreaseItemCount = decreaseItemCount, + onSnackClick = onSnackClick + ) + } + } + item { + SummaryItem( + subtotal = orderLines.map { it.snack.price * it.count }.sum(), + shippingCosts = 369 + ) + } + item { + SnackCollection( + snackCollection = inspiredByCart, + onSnackClick = onSnackClick, + highlight = false + ) + Spacer(Modifier.height(56.dp)) + } + } +} + +@Composable +fun SummaryItem( + subtotal: Long, + shippingCosts: Long, + modifier: Modifier = Modifier +) { + Column(modifier) { + Text( + text = stringResource(MppR.string.cart_summary_header), + style = MaterialTheme.typography.h6, + color = JetsnackTheme.colors.brand, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(horizontal = 24.dp) + .heightIn(min = 56.dp) + .wrapContentHeight() + ) + Row(modifier = Modifier.padding(horizontal = 24.dp)) { + Text( + text = stringResource(MppR.string.cart_subtotal_label), + style = MaterialTheme.typography.body1, + modifier = Modifier + .weight(1f) + .wrapContentWidth(Alignment.Start) + .alignBy(LastBaseline) + ) + Text( + text = formatPrice(subtotal), + style = MaterialTheme.typography.body1, + modifier = Modifier.alignBy(LastBaseline) + ) + } + Row(modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) { + Text( + text = stringResource(MppR.string.cart_shipping_label), + style = MaterialTheme.typography.body1, + modifier = Modifier + .weight(1f) + .wrapContentWidth(Alignment.Start) + .alignBy(LastBaseline) + ) + Text( + text = formatPrice(shippingCosts), + style = MaterialTheme.typography.body1, + modifier = Modifier.alignBy(LastBaseline) + ) + } + Spacer(modifier = Modifier.height(8.dp)) + JetsnackDivider() + Row(modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) { + Text( + text = stringResource(MppR.string.cart_total_label), + style = MaterialTheme.typography.body1, + modifier = Modifier + .weight(1f) + .padding(end = 16.dp) + .wrapContentWidth(Alignment.End) + .alignBy(LastBaseline) + ) + Text( + text = formatPrice(subtotal + shippingCosts), + style = MaterialTheme.typography.subtitle1, + modifier = Modifier.alignBy(LastBaseline) + ) + } + JetsnackDivider() + } +} + +@Composable +private fun CheckoutBar(modifier: Modifier = Modifier) { + Column( + modifier.background( + JetsnackTheme.colors.uiBackground.copy(alpha = AlphaNearOpaque) + ) + ) { + JetsnackDivider() + Row { + Spacer(Modifier.weight(1f)) + JetsnackButton( + onClick = { /* todo */ }, + shape = RectangleShape, + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 8.dp) + .weight(1f) + ) { + Text( + text = stringResource(id = MppR.string.cart_checkout), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Left, + maxLines = 1 + ) + } + } + } +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.kt new file mode 100644 index 0000000000..f4ba3066a7 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.home.cart + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import com.example.jetsnack.* +import com.example.jetsnack.model.OrderLine +import com.example.jetsnack.model.SnackRepo +import com.example.jetsnack.model.SnackbarManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** + * Holds the contents of the cart and allows changes to it. + * + * TODO: Move data to Repository so it can be displayed and changed consistently throughout the app. + */ +class CartViewModel( + private val snackbarManager: SnackbarManager, + snackRepository: SnackRepo +) : JetSnackCartViewModel() { + + private val _orderLines: MutableStateFlow> = + MutableStateFlow(snackRepository.getCart()) + val orderLines: StateFlow> get() = _orderLines + + // Logic to show errors every few requests + private var requestCount = 0 + private fun shouldRandomlyFail(): Boolean = ++requestCount % 5 == 0 + + fun increaseSnackCount(snackId: Long) { + if (!shouldRandomlyFail()) { + val currentCount = _orderLines.value.first { it.snack.id == snackId }.count + updateSnackCount(snackId, currentCount + 1) + } else { + snackbarManager.showMessage(MppR.string.cart_increase_error) + } + } + + fun decreaseSnackCount(snackId: Long) { + if (!shouldRandomlyFail()) { + val currentCount = _orderLines.value.first { it.snack.id == snackId }.count + if (currentCount == 1) { + // remove snack from cart + removeSnack(snackId) + } else { + // update quantity in cart + updateSnackCount(snackId, currentCount - 1) + } + } else { + snackbarManager.showMessage(MppR.string.cart_decrease_error) + } + } + + fun removeSnack(snackId: Long) { + _orderLines.value = _orderLines.value.filter { it.snack.id != snackId } + } + + private fun updateSnackCount(snackId: Long, count: Int) { + _orderLines.value = _orderLines.value.map { + if (it.snack.id == snackId) { + it.copy(count = count) + } else { + it + } + } + } + + companion object // necessary for android (see `provideFactory` method) +} + +expect abstract class JetSnackCartViewModel() { + + @Composable + fun collectOrderLinesAsState(flow: StateFlow>): State> + +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/SwipeDismissItem.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/SwipeDismissItem.kt new file mode 100644 index 0000000000..ff5f68f43f --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/cart/SwipeDismissItem.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.home.cart + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.material.DismissDirection +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.SwipeToDismiss +import androidx.compose.material.rememberDismissState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp + +@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class) +@Composable +/** + * Holds the Swipe to dismiss composable, its animation and the current state + */ +fun SwipeDismissItem( + modifier: Modifier = Modifier, + directions: Set = setOf(DismissDirection.EndToStart), + enter: EnterTransition = expandVertically(), + exit: ExitTransition = shrinkVertically(), + background: @Composable (offset: Dp) -> Unit, + content: @Composable (isDismissed: Boolean) -> Unit, +) { + // Hold the current state from the Swipe to Dismiss composable + val dismissState = rememberDismissState() + // Boolean value used for hiding the item if the current state is dismissed + val isDismissed = dismissState.isDismissed(DismissDirection.EndToStart) + // Returns the swiped value in dp + val offset = with(LocalDensity.current) { dismissState.offset.value.toDp() } + + AnimatedVisibility( + modifier = modifier, + visible = !isDismissed, + enter = enter, + exit = exit + ) { + SwipeToDismiss( + modifier = modifier, + state = dismissState, + directions = directions, + background = { background(offset) }, + dismissContent = { content(isDismissed) } + ) + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Categories.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Categories.kt new file mode 100644 index 0000000000..434217918b --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Categories.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.home.search + +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import com.example.jetsnack.model.SearchCategory +import com.example.jetsnack.model.SearchCategoryCollection +import com.example.jetsnack.ui.components.SnackImage +import com.example.jetsnack.ui.components.VerticalGrid +import com.example.jetsnack.ui.theme.JetsnackTheme +import kotlin.math.max + +@Composable +fun SearchCategories( + categories: List +) { + LazyColumn { + itemsIndexed(categories) { index, collection -> + SearchCategoryCollection(collection, index) + } + } + Spacer(Modifier.height(8.dp)) +} + +@Composable +private fun SearchCategoryCollection( + collection: SearchCategoryCollection, + index: Int, + modifier: Modifier = Modifier +) { + Column(modifier) { + Text( + text = collection.name, + style = MaterialTheme.typography.h6, + color = JetsnackTheme.colors.textPrimary, + modifier = Modifier + .heightIn(min = 56.dp) + .padding(horizontal = 24.dp, vertical = 4.dp) + .wrapContentHeight() + ) + VerticalGrid(Modifier.padding(horizontal = 16.dp), columns = 2) { + val gradient = when (index % 2) { + 0 -> JetsnackTheme.colors.gradient2_2 + else -> JetsnackTheme.colors.gradient2_3 + } + collection.categories.forEach { category -> + SearchCategory( + category = category, + gradient = gradient, + modifier = Modifier.padding(8.dp) + ) + } + } + Spacer(Modifier.height(4.dp)) + } +} + +private val MinImageSize = 134.dp +private val CategoryShape = RoundedCornerShape(10.dp) +private const val CategoryTextProportion = 0.55f + +@Composable +private fun SearchCategory( + category: SearchCategory, + gradient: List, + modifier: Modifier = Modifier +) { + Layout( + modifier = modifier + .aspectRatio(1.45f) + .shadow(elevation = 3.dp, shape = CategoryShape) + .clip(CategoryShape) + .background(Brush.horizontalGradient(gradient)) + .clickable { /* todo */ }, + content = { + Text( + text = category.name, + style = MaterialTheme.typography.subtitle1, + color = JetsnackTheme.colors.textSecondary, + modifier = Modifier + .padding(4.dp) + .padding(start = 8.dp) + ) + SnackImage( + imageUrl = category.imageUrl, + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + } + ) { measurables, constraints -> + // Text given a set proportion of width (which is determined by the aspect ratio) + val textWidth = (constraints.maxWidth * CategoryTextProportion).toInt() + val textPlaceable = measurables[0].measure(Constraints.fixedWidth(textWidth)) + + // Image is sized to the larger of height of item, or a minimum value + // i.e. may appear larger than item (but clipped to the item bounds) + val imageSize = max(MinImageSize.roundToPx(), constraints.maxHeight) + val imagePlaceable = measurables[1].measure(Constraints.fixed(imageSize, imageSize)) + layout( + width = constraints.maxWidth, + height = constraints.minHeight + ) { + textPlaceable.placeRelative( + x = 0, + y = (constraints.maxHeight - textPlaceable.height) / 2 // centered + ) + imagePlaceable.placeRelative( + // image is placed to end of text i.e. will overflow to the end (but be clipped) + x = textWidth, + y = (constraints.maxHeight - imagePlaceable.height) / 2 // centered + ) + } + } +} + +//@Preview +@Composable +private fun SearchCategoryPreview() { + JetsnackTheme { + SearchCategory( + category = SearchCategory( + name = "Desserts", + imageUrl = "" + ), + gradient = JetsnackTheme.colors.gradient3_2 + ) + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Results.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Results.kt new file mode 100644 index 0000000000..654dcad90e --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Results.kt @@ -0,0 +1,219 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.home.search + +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.example.jetsnack.* +import com.example.jetsnack.model.Filter +import com.example.jetsnack.model.Snack +import com.example.jetsnack.model.snacks +import com.example.jetsnack.ui.components.FilterBar +import com.example.jetsnack.ui.components.JetsnackSurface +import com.example.jetsnack.ui.theme.JetsnackTheme + +@Composable +fun SearchResults( + searchResults: List, + filters: List, + onSnackClick: (Long) -> Unit +) { + Column { + FilterBar(filters, onShowFilters = {}) + Text( + text = stringResource(MppR.string.search_count, searchResults.size), + style = MaterialTheme.typography.h6, + color = JetsnackTheme.colors.textPrimary, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp) + ) + LazyColumn { + itemsIndexed(searchResults) { index, snack -> + SearchResult(snack, onSnackClick, index != 0) + } + } + } +} + +@Composable +private fun SearchResult( + snack: Snack, + onSnackClick: (Long) -> Unit, + showDivider: Boolean, + modifier: Modifier = Modifier +) { + // TODO: implement Search Result (we don't have ConstrainLayout in Compose MPP) +// ConstraintLayout( +// modifier = modifier +// .fillMaxWidth() +// .clickable { onSnackClick(snack.id) } +// .padding(horizontal = 24.dp) +// ) { +// val (divider, image, name, tag, priceSpacer, price, add) = createRefs() +// createVerticalChain(name, tag, priceSpacer, price, chainStyle = ChainStyle.Packed) +// if (showDivider) { +// JetsnackDivider( +// Modifier.constrainAs(divider) { +// linkTo(start = parent.start, end = parent.end) +// top.linkTo(parent.top) +// } +// ) +// } +// SnackImage( +// imageUrl = snack.imageUrl, +// contentDescription = null, +// modifier = Modifier +// .size(100.dp) +// .constrainAs(image) { +// linkTo( +// top = parent.top, +// topMargin = 16.dp, +// bottom = parent.bottom, +// bottomMargin = 16.dp +// ) +// start.linkTo(parent.start) +// } +// ) +// Text( +// text = snack.name, +// style = MaterialTheme.typography.subtitle1, +// color = JetsnackTheme.colors.textSecondary, +// modifier = Modifier.constrainAs(name) { +// linkTo( +// start = image.end, +// startMargin = 16.dp, +// end = add.start, +// endMargin = 16.dp, +// bias = 0f +// ) +// } +// ) +// Text( +// text = snack.tagline, +// style = MaterialTheme.typography.body1, +// color = JetsnackTheme.colors.textHelp, +// modifier = Modifier.constrainAs(tag) { +// linkTo( +// start = image.end, +// startMargin = 16.dp, +// end = add.start, +// endMargin = 16.dp, +// bias = 0f +// ) +// } +// ) +// Spacer( +// Modifier +// .height(8.dp) +// .constrainAs(priceSpacer) { +// linkTo(top = tag.bottom, bottom = price.top) +// } +// ) +// Text( +// text = formatPrice(snack.price), +// style = MaterialTheme.typography.subtitle1, +// color = JetsnackTheme.colors.textPrimary, +// modifier = Modifier.constrainAs(price) { +// linkTo( +// start = image.end, +// startMargin = 16.dp, +// end = add.start, +// endMargin = 16.dp, +// bias = 0f +// ) +// } +// ) +// JetsnackButton( +// onClick = { /* todo */ }, +// shape = CircleShape, +// contentPadding = PaddingValues(0.dp), +// modifier = Modifier +// .size(36.dp) +// .constrainAs(add) { +// linkTo(top = parent.top, bottom = parent.bottom) +// end.linkTo(parent.end) +// } +// ) { +// Icon( +// imageVector = Icons.Outlined.Add, +// contentDescription = stringResource(R.string.label_add) +// ) +// } +// } +} + +@Composable +fun NoResults( + query: String, + modifier: Modifier = Modifier +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxSize() + .wrapContentSize() + .padding(24.dp) + ) { + Image( + painterResource(MppR.drawable.empty_state_search), + contentDescription = null + ) + Spacer(Modifier.height(24.dp)) + Text( + text = stringResource(MppR.string.search_no_matches, query), + style = MaterialTheme.typography.subtitle1, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(16.dp)) + Text( + text = stringResource(MppR.string.search_no_matches_retry), + style = MaterialTheme.typography.body2, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } +} + +//@Preview +@Composable +private fun SearchResultPreview() { + JetsnackTheme { + JetsnackSurface { + SearchResult( + snack = snacks[0], + onSnackClick = { }, + showDivider = false + ) + } + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Search.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Search.kt new file mode 100644 index 0000000000..38ce9b1325 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Search.kt @@ -0,0 +1,259 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.home.search + +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Search +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import com.example.jetsnack.* +import com.example.jetsnack.model.Filter +import com.example.jetsnack.model.SearchCategoryCollection +import com.example.jetsnack.model.SearchRepo +import com.example.jetsnack.model.SearchSuggestionGroup +import com.example.jetsnack.model.Snack +import com.example.jetsnack.model.SnackRepo +import com.example.jetsnack.ui.components.JetsnackDivider +import com.example.jetsnack.ui.components.JetsnackSurface +import com.example.jetsnack.ui.snackdetail.jetSnackStatusBarsPadding +import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.utils.mirroringBackIcon + +@Composable +fun Search( + onSnackClick: (Long) -> Unit, + modifier: Modifier = Modifier, + state: SearchState = rememberSearchState() +) { + JetsnackSurface(modifier = modifier.fillMaxSize()) { + Column { + Spacer(modifier = Modifier.jetSnackStatusBarsPadding()) + SearchBar( + query = state.query, + onQueryChange = { state.query = it }, + searchFocused = state.focused, + onSearchFocusChange = { state.focused = it }, + onClearQuery = { state.query = TextFieldValue("") }, + searching = state.searching + ) + JetsnackDivider() + + LaunchedEffect(state.query.text) { + state.searching = true + state.searchResults = SearchRepo.search(state.query.text) + state.searching = false + } + when (state.searchDisplay) { + SearchDisplay.Categories -> SearchCategories(state.categories) + SearchDisplay.Suggestions -> SearchSuggestions( + suggestions = state.suggestions, + onSuggestionSelect = { suggestion -> state.query = TextFieldValue(suggestion) } + ) + SearchDisplay.Results -> SearchResults( + state.searchResults, + state.filters, + onSnackClick + ) + SearchDisplay.NoResults -> NoResults(state.query.text) + } + } + } +} + +enum class SearchDisplay { + Categories, Suggestions, Results, NoResults +} + +@Composable +private fun rememberSearchState( + query: TextFieldValue = TextFieldValue(""), + focused: Boolean = false, + searching: Boolean = false, + categories: List = SearchRepo.getCategories(), + suggestions: List = SearchRepo.getSuggestions(), + filters: List = SnackRepo.getFilters(), + searchResults: List = emptyList() +): SearchState { + return remember { + SearchState( + query = query, + focused = focused, + searching = searching, + categories = categories, + suggestions = suggestions, + filters = filters, + searchResults = searchResults + ) + } +} + +@Stable +class SearchState( + query: TextFieldValue, + focused: Boolean, + searching: Boolean, + categories: List, + suggestions: List, + filters: List, + searchResults: List +) { + var query by mutableStateOf(query) + var focused by mutableStateOf(focused) + var searching by mutableStateOf(searching) + var categories by mutableStateOf(categories) + var suggestions by mutableStateOf(suggestions) + var filters by mutableStateOf(filters) + var searchResults by mutableStateOf(searchResults) + val searchDisplay: SearchDisplay + get() = when { + !focused && query.text.isEmpty() -> SearchDisplay.Categories + focused && query.text.isEmpty() -> SearchDisplay.Suggestions + searchResults.isEmpty() -> SearchDisplay.NoResults + else -> SearchDisplay.Results + } +} + +@Composable +private fun SearchBar( + query: TextFieldValue, + onQueryChange: (TextFieldValue) -> Unit, + searchFocused: Boolean, + onSearchFocusChange: (Boolean) -> Unit, + onClearQuery: () -> Unit, + searching: Boolean, + modifier: Modifier = Modifier +) { + JetsnackSurface( + color = JetsnackTheme.colors.uiFloated, + contentColor = JetsnackTheme.colors.textSecondary, + shape = MaterialTheme.shapes.small, + modifier = modifier + .fillMaxWidth() + .height(56.dp) + .padding(horizontal = 24.dp, vertical = 8.dp) + ) { + Box(Modifier.fillMaxSize()) { + if (query.text.isEmpty()) { + SearchHint() + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxSize() + .wrapContentHeight() + ) { + if (searchFocused) { + IconButton(onClick = onClearQuery) { + Icon( + imageVector = mirroringBackIcon(), + tint = JetsnackTheme.colors.iconPrimary, + contentDescription = stringResource(MppR.string.label_back) + ) + } + } + BasicTextField( + value = query, + onValueChange = onQueryChange, + modifier = Modifier + .weight(1f) + .onFocusChanged { + onSearchFocusChange(it.isFocused) + } + ) + if (searching) { + CircularProgressIndicator( + color = JetsnackTheme.colors.iconPrimary, + modifier = Modifier + .padding(horizontal = 6.dp) + .size(36.dp) + ) + } else { + Spacer(Modifier.width(IconSize)) // balance arrow icon + } + } + } + } +} + +private val IconSize = 48.dp + +@Composable +private fun SearchHint() { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxSize() + .wrapContentSize() + ) { + Icon( + imageVector = Icons.Outlined.Search, + tint = JetsnackTheme.colors.textHelp, + contentDescription = stringResource(MppR.string.label_search) + ) + Spacer(Modifier.width(8.dp)) + Text( + text = stringResource(MppR.string.search_jetsnack), + color = JetsnackTheme.colors.textHelp + ) + } +} + +//@Preview +@Composable +private fun SearchBarPreview() { + JetsnackTheme { + JetsnackSurface { + SearchBar( + query = TextFieldValue(""), + onQueryChange = { }, + searchFocused = false, + onSearchFocusChange = { }, + onClearQuery = { }, + searching = false + ) + } + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Suggestions.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Suggestions.kt new file mode 100644 index 0000000000..6f7562e3e2 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/home/search/Suggestions.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.home.search + +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.example.jetsnack.model.SearchRepo +import com.example.jetsnack.model.SearchSuggestionGroup +import com.example.jetsnack.ui.components.JetsnackSurface +import com.example.jetsnack.ui.theme.JetsnackTheme + +@Composable +fun SearchSuggestions( + suggestions: List, + onSuggestionSelect: (String) -> Unit +) { + LazyColumn { + suggestions.forEach { suggestionGroup -> + item { + SuggestionHeader(suggestionGroup.name) + } + items(suggestionGroup.suggestions) { suggestion -> + Suggestion( + suggestion = suggestion, + onSuggestionSelect = onSuggestionSelect, + modifier = Modifier.fillParentMaxWidth() + ) + } + item { + Spacer(Modifier.height(4.dp)) + } + } + } +} + +@Composable +private fun SuggestionHeader( + name: String, + modifier: Modifier = Modifier +) { + Text( + text = name, + style = MaterialTheme.typography.h6, + color = JetsnackTheme.colors.textPrimary, + modifier = modifier + .heightIn(min = 56.dp) + .padding(horizontal = 24.dp, vertical = 4.dp) + .wrapContentHeight() + ) +} + +@Composable +private fun Suggestion( + suggestion: String, + onSuggestionSelect: (String) -> Unit, + modifier: Modifier = Modifier +) { + Text( + text = suggestion, + style = MaterialTheme.typography.subtitle1, + modifier = modifier + .heightIn(min = 48.dp) + .clickable { onSuggestionSelect(suggestion) } + .padding(start = 24.dp) + .wrapContentSize(Alignment.CenterStart) + ) +} + +//@Preview +@Composable +fun PreviewSuggestions() { + JetsnackTheme { + JetsnackSurface { + SearchSuggestions( + suggestions = SearchRepo.getSuggestions(), + onSuggestionSelect = { } + ) + } + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/snackdetail/SnackDetail.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/snackdetail/SnackDetail.kt new file mode 100644 index 0000000000..9dca0c5776 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/snackdetail/SnackDetail.kt @@ -0,0 +1,406 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.snackdetail + +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.lerp +import androidx.compose.ui.unit.sp +import com.example.jetsnack.* +import com.example.jetsnack.model.Snack +import com.example.jetsnack.model.SnackCollection +import com.example.jetsnack.model.SnackRepo +import com.example.jetsnack.ui.components.JetsnackButton +import com.example.jetsnack.ui.components.JetsnackDivider +import com.example.jetsnack.ui.components.JetsnackSurface +import com.example.jetsnack.ui.components.QuantitySelector +import com.example.jetsnack.ui.components.SnackCollection +import com.example.jetsnack.ui.components.SnackImage +import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.theme.Neutral8 +import com.example.jetsnack.ui.utils.formatPrice +import com.example.jetsnack.ui.utils.mirroringBackIcon +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt +import kotlin.math.roundToLong + +private val BottomBarHeight = 56.dp +private val TitleHeight = 128.dp +private val GradientScroll = 180.dp +private val ImageOverlap = 115.dp +private val MinTitleOffset = 56.dp +private val MinImageOffset = 12.dp +private val MaxTitleOffset = ImageOverlap + MinTitleOffset + GradientScroll +private val ExpandedImageSize = 300.dp +private val CollapsedImageSize = 150.dp +private val HzPadding = Modifier.padding(horizontal = 24.dp) + +@Composable +fun SnackDetail( + snackId: Long, + upPress: () -> Unit, + onSnackClick: (Long) -> Unit, +) { + val snack = remember(snackId) { SnackRepo.getSnack(snackId) } + val related = remember(snackId) { SnackRepo.getRelated(snackId) } + + Box(Modifier.fillMaxSize()) { + val scroll = rememberScrollState(0) + Header() + Body(related, scroll, onSnackClick) + Title(snack) { scroll.value } + Image(snack.imageUrl) { scroll.value } + Up(upPress) + CartBottomBar(modifier = Modifier.align(Alignment.BottomCenter)) + } +} + +@Composable +private fun Header() { + Spacer( + modifier = Modifier + .height(280.dp) + .fillMaxWidth() + .background(Brush.horizontalGradient(JetsnackTheme.colors.tornado1)) + ) +} + +@Composable +private fun Up(upPress: () -> Unit) { + IconButton( + onClick = upPress, + modifier = Modifier + .jetSnackStatusBarsPadding() + .padding(horizontal = 16.dp, vertical = 10.dp) + .size(36.dp) + .background( + color = Neutral8.copy(alpha = 0.32f), + shape = CircleShape + ) + ) { + Icon( + imageVector = mirroringBackIcon(), + tint = JetsnackTheme.colors.iconInteractive, + contentDescription = stringResource(MppR.string.label_back) + ) + } +} + +@Composable +private fun Body( + related: List, + scroll: ScrollState, + onSnackClick: (Long) -> Unit, +) { + Column { + Spacer( + modifier = Modifier + .fillMaxWidth() + .jetSnackStatusBarsPadding() + .height(MinTitleOffset) + ) + Column( + modifier = Modifier.verticalScroll(scroll) + ) { + Spacer(Modifier.height(GradientScroll)) + JetsnackSurface(Modifier.fillMaxWidth()) { + Column { + Spacer(Modifier.height(ImageOverlap)) + Spacer(Modifier.height(TitleHeight)) + + Spacer(Modifier.height(16.dp)) + Text( + text = stringResource(MppR.string.detail_header), + style = MaterialTheme.typography.overline, + color = JetsnackTheme.colors.textHelp, + modifier = HzPadding + ) + Spacer(Modifier.height(16.dp)) + var seeMore by remember { mutableStateOf(true) } + Text( + text = stringResource(MppR.string.detail_placeholder), + style = MaterialTheme.typography.body1, + color = JetsnackTheme.colors.textHelp, + maxLines = if (seeMore) 5 else Int.MAX_VALUE, + overflow = TextOverflow.Ellipsis, + modifier = HzPadding + ) + val textButton = if (seeMore) { + stringResource(id = MppR.string.see_more) + } else { + stringResource(id = MppR.string.see_less) + } + Text( + text = textButton, + style = MaterialTheme.typography.button, + textAlign = TextAlign.Center, + color = JetsnackTheme.colors.textLink, + modifier = Modifier + .heightIn(20.dp) + .fillMaxWidth() + .padding(top = 15.dp) + .clickable { + seeMore = !seeMore + } + ) + Spacer(Modifier.height(40.dp)) + Text( + text = stringResource(MppR.string.ingredients), + style = MaterialTheme.typography.overline, + color = JetsnackTheme.colors.textHelp, + modifier = HzPadding + ) + Spacer(Modifier.height(4.dp)) + Text( + text = stringResource(MppR.string.ingredients_list), + style = MaterialTheme.typography.body1, + color = JetsnackTheme.colors.textHelp, + modifier = HzPadding + ) + + Spacer(Modifier.height(16.dp)) + JetsnackDivider() + + related.forEach { snackCollection -> + key(snackCollection.id) { + SnackCollection( + snackCollection = snackCollection, + onSnackClick = onSnackClick, + highlight = false + ) + } + } + + Spacer( + modifier = Modifier + .padding(bottom = BottomBarHeight) + .jetSnackNavigationBarsPadding() + .height(8.dp) + ) + } + } + } + } +} + +@Composable +private fun Title(snack: Snack, scrollProvider: () -> Int) { + val maxOffset = with(LocalDensity.current) { MaxTitleOffset.toPx() } + val minOffset = with(LocalDensity.current) { MinTitleOffset.toPx() } + + Column( + verticalArrangement = Arrangement.Bottom, + modifier = Modifier + .heightIn(min = TitleHeight) + .jetSnackStatusBarsPadding() + .offset { + val scroll = scrollProvider() + val offset = (maxOffset - scroll).coerceAtLeast(minOffset) + IntOffset(x = 0, y = offset.toInt()) + } + .background(color = JetsnackTheme.colors.uiBackground) + ) { + Spacer(Modifier.height(16.dp)) + Text( + text = snack.name, + style = MaterialTheme.typography.h4, + color = JetsnackTheme.colors.textSecondary, + modifier = HzPadding + ) + Text( + text = snack.tagline, + style = MaterialTheme.typography.subtitle2, + fontSize = 20.sp, + color = JetsnackTheme.colors.textHelp, + modifier = HzPadding + ) + Spacer(Modifier.height(4.dp)) + Text( + text = formatPrice(snack.price), + style = MaterialTheme.typography.h6, + color = JetsnackTheme.colors.textPrimary, + modifier = HzPadding + ) + + Spacer(Modifier.height(8.dp)) + JetsnackDivider() + } +} + +@Composable +private fun Image( + imageUrl: String, + scrollProvider: () -> Int +) { + val collapseRange = with(LocalDensity.current) { (MaxTitleOffset - MinTitleOffset).toPx() } + val collapseFractionProvider = { + (scrollProvider() / collapseRange).coerceIn(0f, 1f) + } + + CollapsingImageLayout( + collapseFractionProvider = collapseFractionProvider, + modifier = HzPadding.then(Modifier.jetSnackStatusBarsPadding()) + ) { + SnackImage( + imageUrl = imageUrl, + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + } +} + +@Composable +private fun CollapsingImageLayout( + collapseFractionProvider: () -> Float, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Layout( + modifier = modifier, + content = content + ) { measurables, constraints -> + check(measurables.size == 1) + + val collapseFraction = collapseFractionProvider() + + val imageMaxSize = min(ExpandedImageSize.roundToPx(), constraints.maxWidth) + val imageMinSize = max(CollapsedImageSize.roundToPx(), constraints.minWidth) + val imageWidth = lerp(imageMaxSize, imageMinSize, collapseFraction) + val imagePlaceable = measurables[0].measure(Constraints.fixed(imageWidth, imageWidth)) + + val imageY = lerp(MinTitleOffset, MinImageOffset, collapseFraction).roundToPx() + val imageX = lerp( + (constraints.maxWidth - imageWidth) / 2, // centered when expanded + constraints.maxWidth - imageWidth, // right aligned when collapsed + collapseFraction + ) + layout( + width = constraints.maxWidth, + height = imageY + imageWidth + ) { + imagePlaceable.placeRelative(imageX, imageY) + } + } +} + +fun lerp(start: Float, stop: Float, fraction: Float): Float { + return (1 - fraction) * start + fraction * stop +} + +/** + * Linearly interpolate between [start] and [stop] with [fraction] fraction between them. + */ +fun lerp(start: Int, stop: Int, fraction: Float): Int { + return start + ((stop - start) * fraction.toDouble()).roundToInt() +} + +/** + * Linearly interpolate between [start] and [stop] with [fraction] fraction between them. + */ +fun lerp(start: Long, stop: Long, fraction: Float): Long { + return start + ((stop - start) * fraction.toDouble()).roundToLong() +} + +@Composable +private fun CartBottomBar(modifier: Modifier = Modifier) { + val (count, updateCount) = remember { mutableStateOf(1) } + JetsnackSurface(modifier) { + Column { + JetsnackDivider() + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .jetSnackNavigationBarsPadding() + .then(HzPadding) + .heightIn(min = BottomBarHeight) + ) { + QuantitySelector( + count = count, + decreaseItemCount = { if (count > 0) updateCount(count - 1) }, + increaseItemCount = { updateCount(count + 1) } + ) + Spacer(Modifier.width(16.dp)) + JetsnackButton( + onClick = { /* todo */ }, + modifier = Modifier.weight(1f) + ) { + Text( + text = stringResource(MppR.string.add_to_cart), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + maxLines = 1 + ) + } + } + } + } +} + +expect fun Modifier.jetSnackNavigationBarsPadding(): Modifier +expect fun Modifier.jetSnackStatusBarsPadding(): Modifier +expect fun Modifier.jetSnackSystemBarsPadding(): Modifier + +//@Preview +@Composable +private fun SnackDetailPreview() { + JetsnackTheme { + SnackDetail( + snackId = 1L, + upPress = { }, + onSnackClick = { } + ) + } +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Color.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Color.kt new file mode 100644 index 0000000000..eb99c98163 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Color.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.theme + +import androidx.compose.ui.graphics.Color + +val Shadow11 = Color(0xff001787) +val Shadow10 = Color(0xff00119e) +val Shadow9 = Color(0xff0009b3) +val Shadow8 = Color(0xff0200c7) +val Shadow7 = Color(0xff0e00d7) +val Shadow6 = Color(0xff2a13e4) +val Shadow5 = Color(0xff4b30ed) +val Shadow4 = Color(0xff7057f5) +val Shadow3 = Color(0xff9b86fa) +val Shadow2 = Color(0xffc8bbfd) +val Shadow1 = Color(0xffded6fe) +val Shadow0 = Color(0xfff4f2ff) + +val Ocean11 = Color(0xff005687) +val Ocean10 = Color(0xff006d9e) +val Ocean9 = Color(0xff0087b3) +val Ocean8 = Color(0xff00a1c7) +val Ocean7 = Color(0xff00b9d7) +val Ocean6 = Color(0xff13d0e4) +val Ocean5 = Color(0xff30e2ed) +val Ocean4 = Color(0xff57eff5) +val Ocean3 = Color(0xff86f7fa) +val Ocean2 = Color(0xffbbfdfd) +val Ocean1 = Color(0xffd6fefe) +val Ocean0 = Color(0xfff2ffff) + +val Lavender11 = Color(0xff170085) +val Lavender10 = Color(0xff23009e) +val Lavender9 = Color(0xff3300b3) +val Lavender8 = Color(0xff4400c7) +val Lavender7 = Color(0xff5500d7) +val Lavender6 = Color(0xff6f13e4) +val Lavender5 = Color(0xff8a30ed) +val Lavender4 = Color(0xffa557f5) +val Lavender3 = Color(0xffc186fa) +val Lavender2 = Color(0xffdebbfd) +val Lavender1 = Color(0xffebd6fe) +val Lavender0 = Color(0xfff9f2ff) + +val Rose11 = Color(0xff7f0054) +val Rose10 = Color(0xff97005c) +val Rose9 = Color(0xffaf0060) +val Rose8 = Color(0xffc30060) +val Rose7 = Color(0xffd4005d) +val Rose6 = Color(0xffe21365) +val Rose5 = Color(0xffec3074) +val Rose4 = Color(0xfff4568b) +val Rose3 = Color(0xfff985aa) +val Rose2 = Color(0xfffdbbcf) +val Rose1 = Color(0xfffed6e2) +val Rose0 = Color(0xfffff2f6) + +val Neutral8 = Color(0xff121212) +val Neutral7 = Color(0xde000000) +val Neutral6 = Color(0x99000000) +val Neutral5 = Color(0x61000000) +val Neutral4 = Color(0x1f000000) +val Neutral3 = Color(0x1fffffff) +val Neutral2 = Color(0x61ffffff) +val Neutral1 = Color(0xbdffffff) +val Neutral0 = Color(0xffffffff) + +val FunctionalRed = Color(0xffd00036) +val FunctionalRedDark = Color(0xffea6d7e) +val FunctionalGreen = Color(0xff52c41a) +val FunctionalGrey = Color(0xfff6f6f6) +val FunctionalDarkGrey = Color(0xff2e2e2e) + +const val AlphaNearOpaque = 0.95f diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Shape.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Shape.kt new file mode 100644 index 0000000000..76d6b842d4 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Shape.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(percent = 50), + medium = RoundedCornerShape(20.dp), + large = RoundedCornerShape(0.dp) +) diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Theme.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Theme.kt new file mode 100644 index 0000000000..33032a76c8 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Theme.kt @@ -0,0 +1,310 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.Colors +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +//import com.google.accompanist.systemuicontroller.rememberSystemUiController + +private val LightColorPalette = JetsnackColors( + brand = Shadow5, + brandSecondary = Ocean3, + uiBackground = Neutral0, + uiBorder = Neutral4, + uiFloated = FunctionalGrey, + textSecondary = Neutral7, + textHelp = Neutral6, + textInteractive = Neutral0, + textLink = Ocean11, + iconSecondary = Neutral7, + iconInteractive = Neutral0, + iconInteractiveInactive = Neutral1, + error = FunctionalRed, + gradient6_1 = listOf(Shadow4, Ocean3, Shadow2, Ocean3, Shadow4), + gradient6_2 = listOf(Rose4, Lavender3, Rose2, Lavender3, Rose4), + gradient3_1 = listOf(Shadow2, Ocean3, Shadow4), + gradient3_2 = listOf(Rose2, Lavender3, Rose4), + gradient2_1 = listOf(Shadow4, Shadow11), + gradient2_2 = listOf(Ocean3, Shadow3), + gradient2_3 = listOf(Lavender3, Rose2), + tornado1 = listOf(Shadow4, Ocean3), + isDark = false +) + +private val DarkColorPalette = JetsnackColors( + brand = Shadow1, + brandSecondary = Ocean2, + uiBackground = Neutral8, + uiBorder = Neutral3, + uiFloated = FunctionalDarkGrey, + textPrimary = Shadow1, + textSecondary = Neutral0, + textHelp = Neutral1, + textInteractive = Neutral7, + textLink = Ocean2, + iconPrimary = Shadow1, + iconSecondary = Neutral0, + iconInteractive = Neutral7, + iconInteractiveInactive = Neutral6, + error = FunctionalRedDark, + gradient6_1 = listOf(Shadow5, Ocean7, Shadow9, Ocean7, Shadow5), + gradient6_2 = listOf(Rose11, Lavender7, Rose8, Lavender7, Rose11), + gradient3_1 = listOf(Shadow9, Ocean7, Shadow5), + gradient3_2 = listOf(Rose8, Lavender7, Rose11), + gradient2_1 = listOf(Ocean3, Shadow3), + gradient2_2 = listOf(Ocean4, Shadow2), + gradient2_3 = listOf(Lavender3, Rose3), + tornado1 = listOf(Shadow4, Ocean3), + isDark = true +) + +@Composable +fun JetsnackTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colors = if (darkTheme) DarkColorPalette else LightColorPalette + +// TODO: implement setSystemBarsColor for android! +// val sysUiController = rememberSystemUiController() +// SideEffect { +// sysUiController.setSystemBarsColor( +// color = colors.uiBackground.copy(alpha = AlphaNearOpaque) +// ) +// } + + ProvideJetsnackColors(colors) { + MaterialTheme( + colors = debugColors(darkTheme), + typography = Typography, + shapes = Shapes, + content = content + ) + } +} + +object JetsnackTheme { + val colors: JetsnackColors + @Composable + get() = LocalJetsnackColors.current +} + +/** + * Jetsnack custom Color Palette + */ +@Stable +class JetsnackColors( + gradient6_1: List, + gradient6_2: List, + gradient3_1: List, + gradient3_2: List, + gradient2_1: List, + gradient2_2: List, + gradient2_3: List, + brand: Color, + brandSecondary: Color, + uiBackground: Color, + uiBorder: Color, + uiFloated: Color, + interactivePrimary: List = gradient2_1, + interactiveSecondary: List = gradient2_2, + interactiveMask: List = gradient6_1, + textPrimary: Color = brand, + textSecondary: Color, + textHelp: Color, + textInteractive: Color, + textLink: Color, + tornado1: List, + iconPrimary: Color = brand, + iconSecondary: Color, + iconInteractive: Color, + iconInteractiveInactive: Color, + error: Color, + notificationBadge: Color = error, + isDark: Boolean +) { + var gradient6_1 by mutableStateOf(gradient6_1) + private set + var gradient6_2 by mutableStateOf(gradient6_2) + private set + var gradient3_1 by mutableStateOf(gradient3_1) + private set + var gradient3_2 by mutableStateOf(gradient3_2) + private set + var gradient2_1 by mutableStateOf(gradient2_1) + private set + var gradient2_2 by mutableStateOf(gradient2_2) + private set + var gradient2_3 by mutableStateOf(gradient2_3) + private set + var brand by mutableStateOf(brand) + private set + var brandSecondary by mutableStateOf(brandSecondary) + private set + var uiBackground by mutableStateOf(uiBackground) + private set + var uiBorder by mutableStateOf(uiBorder) + private set + var uiFloated by mutableStateOf(uiFloated) + private set + var interactivePrimary by mutableStateOf(interactivePrimary) + private set + var interactiveSecondary by mutableStateOf(interactiveSecondary) + private set + var interactiveMask by mutableStateOf(interactiveMask) + private set + var textPrimary by mutableStateOf(textPrimary) + private set + var textSecondary by mutableStateOf(textSecondary) + private set + var textHelp by mutableStateOf(textHelp) + private set + var textInteractive by mutableStateOf(textInteractive) + private set + var tornado1 by mutableStateOf(tornado1) + private set + var textLink by mutableStateOf(textLink) + private set + var iconPrimary by mutableStateOf(iconPrimary) + private set + var iconSecondary by mutableStateOf(iconSecondary) + private set + var iconInteractive by mutableStateOf(iconInteractive) + private set + var iconInteractiveInactive by mutableStateOf(iconInteractiveInactive) + private set + var error by mutableStateOf(error) + private set + var notificationBadge by mutableStateOf(notificationBadge) + private set + var isDark by mutableStateOf(isDark) + private set + + fun update(other: JetsnackColors) { + gradient6_1 = other.gradient6_1 + gradient6_2 = other.gradient6_2 + gradient3_1 = other.gradient3_1 + gradient3_2 = other.gradient3_2 + gradient2_1 = other.gradient2_1 + gradient2_2 = other.gradient2_2 + gradient2_3 = other.gradient2_3 + brand = other.brand + brandSecondary = other.brandSecondary + uiBackground = other.uiBackground + uiBorder = other.uiBorder + uiFloated = other.uiFloated + interactivePrimary = other.interactivePrimary + interactiveSecondary = other.interactiveSecondary + interactiveMask = other.interactiveMask + textPrimary = other.textPrimary + textSecondary = other.textSecondary + textHelp = other.textHelp + textInteractive = other.textInteractive + textLink = other.textLink + tornado1 = other.tornado1 + iconPrimary = other.iconPrimary + iconSecondary = other.iconSecondary + iconInteractive = other.iconInteractive + iconInteractiveInactive = other.iconInteractiveInactive + error = other.error + notificationBadge = other.notificationBadge + isDark = other.isDark + } + + fun copy(): JetsnackColors = JetsnackColors( + gradient6_1 = gradient6_1, + gradient6_2 = gradient6_2, + gradient3_1 = gradient3_1, + gradient3_2 = gradient3_2, + gradient2_1 = gradient2_1, + gradient2_2 = gradient2_2, + gradient2_3 = gradient2_3, + brand = brand, + brandSecondary = brandSecondary, + uiBackground = uiBackground, + uiBorder = uiBorder, + uiFloated = uiFloated, + interactivePrimary = interactivePrimary, + interactiveSecondary = interactiveSecondary, + interactiveMask = interactiveMask, + textPrimary = textPrimary, + textSecondary = textSecondary, + textHelp = textHelp, + textInteractive = textInteractive, + textLink = textLink, + tornado1 = tornado1, + iconPrimary = iconPrimary, + iconSecondary = iconSecondary, + iconInteractive = iconInteractive, + iconInteractiveInactive = iconInteractiveInactive, + error = error, + notificationBadge = notificationBadge, + isDark = isDark, + ) +} + +@Composable +fun ProvideJetsnackColors( + colors: JetsnackColors, + content: @Composable () -> Unit +) { + val colorPalette = remember { + // Explicitly creating a new object here so we don't mutate the initial [colors] + // provided, and overwrite the values set in it. + colors.copy() + } + colorPalette.update(colors) + CompositionLocalProvider(LocalJetsnackColors provides colorPalette, content = content) +} + +private val LocalJetsnackColors = staticCompositionLocalOf { + error("No JetsnackColorPalette provided") +} + +/** + * A Material [Colors] implementation which sets all colors to [debugColor] to discourage usage of + * [MaterialTheme.colors] in preference to [JetsnackTheme.colors]. + */ +fun debugColors( + darkTheme: Boolean, + debugColor: Color = Color.Magenta +) = Colors( + primary = debugColor, + primaryVariant = debugColor, + secondary = debugColor, + secondaryVariant = debugColor, + background = debugColor, + surface = debugColor, + error = debugColor, + onPrimary = debugColor, + onSecondary = debugColor, + onBackground = debugColor, + onSurface = debugColor, + onError = debugColor, + isLight = !darkTheme +) diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Type.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Type.kt new file mode 100644 index 0000000000..195efcff4f --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/theme/Type.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.theme + +import androidx.compose.material.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +var Montserrat: FontFamily? = null // init in platform code +var Karla: FontFamily? = null // init in platform code + +val Typography by lazy { + Typography( + h1 = TextStyle( + fontFamily = Montserrat, + fontSize = 96.sp, + fontWeight = FontWeight.Light, + lineHeight = 117.sp, + letterSpacing = (-1.5).sp, + ), + h2 = TextStyle( + fontFamily = Montserrat, + fontSize = 60.sp, + fontWeight = FontWeight.Light, + lineHeight = 73.sp, + letterSpacing = (-0.5).sp + ), + h3 = TextStyle( + fontFamily = Montserrat, + fontSize = 48.sp, + fontWeight = FontWeight.Normal, + lineHeight = 59.sp + ), + h4 = TextStyle( + fontFamily = Montserrat, + fontSize = 30.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 37.sp + ), + h5 = TextStyle( + fontFamily = Montserrat, + fontSize = 24.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 29.sp + ), + h6 = TextStyle( + fontFamily = Montserrat, + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 24.sp + ), + subtitle1 = TextStyle( + fontFamily = Montserrat, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 24.sp, + letterSpacing = 0.15.sp + ), + subtitle2 = TextStyle( + fontFamily = Karla, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + lineHeight = 24.sp, + letterSpacing = 0.1.sp + ), + body1 = TextStyle( + fontFamily = Karla, + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + lineHeight = 28.sp, + letterSpacing = 0.15.sp + ), + body2 = TextStyle( + fontFamily = Montserrat, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + lineHeight = 20.sp, + letterSpacing = 0.25.sp + ), + button = TextStyle( + fontFamily = Montserrat, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 16.sp, + letterSpacing = 1.25.sp + ), + caption = TextStyle( + fontFamily = Karla, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + lineHeight = 16.sp, + letterSpacing = 0.4.sp + ), + overline = TextStyle( + fontFamily = Montserrat, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 16.sp, + letterSpacing = 1.sp + ) + ) +} diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/Currency.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/Currency.kt new file mode 100644 index 0000000000..58428b5c6d --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/Currency.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.utils + +expect fun formatPrice(price: Long): String diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/Rtl.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/Rtl.kt new file mode 100644 index 0000000000..0bff71cd0a --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/Rtl.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.utils + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material.icons.outlined.ArrowForward +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.LayoutDirection + +/** + * Returns the correct icon based on the current layout direction. + */ +@Composable +fun mirroringIcon(ltrIcon: ImageVector, rtlIcon: ImageVector): ImageVector = + if (LocalLayoutDirection.current == LayoutDirection.Ltr) ltrIcon else rtlIcon + +/** + * Returns the correct back navigation icon based on the current layout direction. + */ +@Composable +fun mirroringBackIcon() = mirroringIcon( + ltrIcon = Icons.Outlined.ArrowBack, rtlIcon = Icons.Outlined.ArrowForward +) diff --git a/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/SystemUi.kt b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/SystemUi.kt new file mode 100644 index 0000000000..2b28986900 --- /dev/null +++ b/examples/jetsnack/common/src/commonMain/kotlin/com/example/jetsnack/ui/utils/SystemUi.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.utils + +/** + * Moved to https://google.github.io/accompanist/systemuicontroller/ + */ diff --git a/examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/model/createRandomUUID.kt b/examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/model/createRandomUUID.kt new file mode 100644 index 0000000000..cf4e30a02c --- /dev/null +++ b/examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/model/createRandomUUID.kt @@ -0,0 +1,7 @@ +package com.example.jetsnack.model + +import java.util.* + +actual fun createRandomUUID(): Long { + return UUID.randomUUID().mostSignificantBits +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt b/examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt new file mode 100644 index 0000000000..2aa7db9147 --- /dev/null +++ b/examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt @@ -0,0 +1,53 @@ +package com.example.jetsnack.ui.components + +import androidx.compose.animation.* +import androidx.compose.animation.core.TweenSpec +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap +import androidx.compose.ui.layout.ContentScale +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.net.URL +import javax.imageio.ImageIO + + +private val imagesCache = mutableMapOf() + +@OptIn(ExperimentalAnimationApi::class) +@Composable +actual fun SnackAsyncImage(imageUrl: String, contentDescription: String?, modifier: Modifier) { + var img: ImageBitmap? by remember(imageUrl) { mutableStateOf(null) } + + + AnimatedContent(img, transitionSpec = { + fadeIn(TweenSpec()) with fadeOut(TweenSpec()) + }) { + if (img != null) { + Image(img!!, contentDescription = contentDescription, modifier = modifier, contentScale = ContentScale.Crop) + } else { + Box(modifier = modifier) + } + } + + LaunchedEffect(imageUrl) { + if (imagesCache.contains(imageUrl)) { + img = imagesCache[imageUrl] + } else { + withContext(Dispatchers.IO) { + img = try { + ImageIO.read(URL(imageUrl)).toComposeImageBitmap().also { + imagesCache[imageUrl] = it + img = it + } + } catch (e: Throwable) { + e.printStackTrace() + null + } + } + } + } +} diff --git a/examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.kt b/examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.kt new file mode 100644 index 0000000000..c3dc9dfbaa --- /dev/null +++ b/examples/jetsnack/common/src/desktopMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.kt @@ -0,0 +1,10 @@ +package com.example.jetsnack.ui.utils + +import java.math.BigDecimal +import java.text.NumberFormat + +actual fun formatPrice(price: Long): String { + return NumberFormat.getCurrencyInstance().format( + BigDecimal(price).movePointLeft(2) + ) +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/model/createRandomUUID.ios.kt b/examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/model/createRandomUUID.ios.kt new file mode 100644 index 0000000000..6720c3404d --- /dev/null +++ b/examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/model/createRandomUUID.ios.kt @@ -0,0 +1,13 @@ +package com.example.jetsnack.model + +import kotlinx.cinterop.ExperimentalForeignApi +import platform.CoreFoundation.CFUUIDCreate +import platform.CoreFoundation.CFUUIDCreateString + +@OptIn(ExperimentalForeignApi::class) +actual fun createRandomUUID(): Long { + val uuidRef = CFUUIDCreate(null) + val uuidStringRef = CFUUIDCreateString(null, uuidRef) as String + val uuidStr: String = uuidStringRef.replace("-", "") + return uuidStr.substring(uuidStr.length - 16).toLong(16) +} diff --git a/examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.ios.kt b/examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.ios.kt new file mode 100644 index 0000000000..1e8bdc408d --- /dev/null +++ b/examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.ios.kt @@ -0,0 +1,86 @@ +package com.example.jetsnack.ui.components + +import androidx.compose.animation.* +import androidx.compose.animation.core.TweenSpec +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap +import androidx.compose.ui.layout.ContentScale +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.usePinned +import kotlinx.coroutines.* +import org.jetbrains.skia.Image +import platform.Foundation.* +import platform.posix.memcpy +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +private val imagesCache = mutableMapOf() + +@OptIn(ExperimentalAnimationApi::class) +@Composable +actual fun SnackAsyncImage(imageUrl: String, contentDescription: String?, modifier: Modifier) { + var img: ImageBitmap? by remember(imageUrl) { mutableStateOf(null) } + + + AnimatedContent(img, transitionSpec = { + fadeIn(TweenSpec()) with fadeOut(TweenSpec()) + }) { + if (img != null) { + Image(img!!, contentDescription = contentDescription, modifier = modifier, contentScale = ContentScale.Crop) + } else { + Box(modifier = modifier) + } + } + + LaunchedEffect(imageUrl) { + if (imagesCache.contains(imageUrl)) { + img = imagesCache[imageUrl] + } else { + withContext(Dispatchers.IO) { + img = try { + loadImage(imageUrl).also { + imagesCache[imageUrl] = it + img = it + } + } catch (e: Throwable) { + e.printStackTrace() + null + } + } + } + } +} + +@OptIn(ExperimentalForeignApi::class) +suspend fun loadImage(url: String): ImageBitmap = suspendCancellableCoroutine { continuation -> + val nsUrl = NSURL(string = url) + val task = NSURLSession.sharedSession.dataTaskWithURL(nsUrl) { data, response, error -> + if (data != null) { + val byteArray = ByteArray(data.length.toInt()).apply { + usePinned { + memcpy( + it.addressOf(0), + data.bytes, + data.length + ) + } + } + + continuation.resume(Image.makeFromEncoded(byteArray).toComposeImageBitmap()) + } else { + error?.let { + continuation.resumeWithException(Exception(it.localizedDescription)) + } + } + } + + task.resume() + continuation.invokeOnCancellation { + task.cancel() + } +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/main.ios.kt b/examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/main.ios.kt new file mode 100644 index 0000000000..703480ffa1 --- /dev/null +++ b/examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/main.ios.kt @@ -0,0 +1,10 @@ +package com.example.jetsnack.ui + +import androidx.compose.ui.window.ComposeUIViewController +import com.example.jetsnack.JetSnackAppEntryPoint +import platform.UIKit.UIViewController + +fun MainViewController(): UIViewController = + ComposeUIViewController { + JetSnackAppEntryPoint() + } \ No newline at end of file diff --git a/examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.ios.kt b/examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.ios.kt new file mode 100644 index 0000000000..3064cf4a4e --- /dev/null +++ b/examples/jetsnack/common/src/iosMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.ios.kt @@ -0,0 +1,14 @@ +package com.example.jetsnack.ui.utils + +import platform.Foundation.* + +actual fun formatPrice(price: Long): String { + val priceAsDouble = price / 100.0 + + val formatter = NSNumberFormatter() + formatter.setLocale(NSLocale.currentLocale) + formatter.numberStyle = NSNumberFormatterCurrencyStyle + + val numberPrice = NSNumber.numberWithDouble(priceAsDouble) + return formatter.stringFromNumber(numberPrice) ?: "" +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/DesktopApp.kt b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/DesktopApp.kt new file mode 100644 index 0000000000..ea6e5518e7 --- /dev/null +++ b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/DesktopApp.kt @@ -0,0 +1,12 @@ +package com.example.jetsnack + +//import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.runtime.Composable +import com.example.jetsnack.ui.JetsnackApp + +//@Preview +@Composable +fun AppPreview() { + JetsnackApp() +// App() +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/JetsnakAppEntryPoint.kt b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/JetsnakAppEntryPoint.kt new file mode 100644 index 0000000000..46521120c3 --- /dev/null +++ b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/JetsnakAppEntryPoint.kt @@ -0,0 +1,15 @@ +package com.example.jetsnack + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import com.example.jetsnack.ui.JetsnackApp + +@Composable +fun JetSnackAppEntryPoint() { + CompositionLocalProvider( + strsLocal provides buildStingsResources(), + pluralsLocal provides buildPluralResources() + ) { + JetsnackApp() + } +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/drawableResources.desktop.kt b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/drawableResources.desktop.kt new file mode 100644 index 0000000000..43247412f2 --- /dev/null +++ b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/drawableResources.desktop.kt @@ -0,0 +1,23 @@ +package com.example.jetsnack + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.VectorPainter +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import com.example.jetsnack.MppR +import com.example.jetsnack.ui.myiconpack.EmptyStateSearch +import org.jetbrains.skiko.currentNanoTime + +@Composable +actual fun painterResource(id: Int): Painter { + return when(id) { + MppR.drawable.empty_state_search -> rememberVectorPainter(EmptyStateSearch) + else -> TODO() + } +} + +private var lastId = currentNanoTime().toInt() + + +private val _empty_state_search = lastId++ +actual val MppR.drawable.empty_state_search: Int get() = _empty_state_search \ No newline at end of file diff --git a/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/initStringResource.kt b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/initStringResource.kt new file mode 100644 index 0000000000..2600f190a6 --- /dev/null +++ b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/initStringResource.kt @@ -0,0 +1,84 @@ +package com.example.jetsnack + +fun buildStingsResources(): Map { + val strs = mutableMapOf() + 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) { + + // 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 { + val plurals = mutableMapOf() + val ps = MppR.plurals + + plurals[ps.cart_order_count] = PluralResource(buildMap { + this["one"] = "%1d item" + this["other"] = "%1d items" + }) + + return plurals +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/stringResource.kt b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/stringResource.kt new file mode 100644 index 0000000000..fe94abce16 --- /dev/null +++ b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/stringResource.kt @@ -0,0 +1,176 @@ +@file:Suppress("PrivatePropertyName") + +package com.example.jetsnack + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocal +import androidx.compose.runtime.compositionLocalOf +import org.jetbrains.skiko.currentNanoTime + + +val strsLocal = compositionLocalOf { emptyMap() } // intId to String +val pluralsLocal = compositionLocalOf { emptyMap() } + +@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 \ No newline at end of file diff --git a/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/JetsnackScaffoldContent.kt b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/JetsnackScaffoldContent.kt new file mode 100644 index 0000000000..c2d552a70a --- /dev/null +++ b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/JetsnackScaffoldContent.kt @@ -0,0 +1,86 @@ +package com.example.jetsnack.ui + +import androidx.compose.animation.* +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshots.Snapshot +import androidx.compose.ui.Modifier +import com.example.jetsnack.ui.home.CartTodo +import com.example.jetsnack.ui.home.Feed +import com.example.jetsnack.ui.home.HomeSections +import com.example.jetsnack.ui.home.Profile +import com.example.jetsnack.ui.home.cart.Cart +import com.example.jetsnack.ui.home.search.Search +import com.example.jetsnack.ui.snackdetail.SnackDetail + +@OptIn(ExperimentalAnimationApi::class) +@Composable +actual fun JetsnackScaffoldContent( + innerPaddingModifier: PaddingValues, + appState: MppJetsnackAppState +) { + + when (appState.currentRoute) { + HomeSections.FEED.route -> { + Feed( + onSnackClick = appState::navigateToSnackDetail, + modifier = Modifier.padding(innerPaddingModifier) + ) + } + + HomeSections.SEARCH.route -> { + Search( + onSnackClick = appState::navigateToSnackDetail, + modifier = Modifier.padding(innerPaddingModifier) + ) + } + + HomeSections.CART.route -> { + Cart( + onSnackClick = appState::navigateToSnackDetail, + modifier = Modifier.padding(innerPaddingModifier) + ) + } + + HomeSections.PROFILE.route -> { + Profile(modifier = Modifier.padding(innerPaddingModifier)) + } + + else -> { + val snackId = appState.currentRoute?.takeIf { + it.startsWith(MainDestinations.SNACK_DETAIL_ROUTE + "/") + }?.let { + it.split("/")[1].toLongOrNull() + } + if (snackId != null) { + SnackDetail(snackId, appState::upPress, appState::navigateToSnackDetail) + } + } + } +} + +class NavigationStack(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() +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/MppJetsnackAppState.kt b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/MppJetsnackAppState.kt new file mode 100644 index 0000000000..bb14aab595 --- /dev/null +++ b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/MppJetsnackAppState.kt @@ -0,0 +1,52 @@ +package com.example.jetsnack.ui + +import androidx.compose.material.ScaffoldState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.* +import com.example.jetsnack.model.SnackbarManager +import com.example.jetsnack.ui.home.HomeSections +import kotlinx.coroutines.CoroutineScope + +@Stable +actual class MppJetsnackAppState( + actual val scaffoldState: ScaffoldState, + actual val snackbarManager: SnackbarManager, + actual val coroutineScope: CoroutineScope, +) { + actual val bottomBarTabs: Array + 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) + } +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/CartTodo.kt b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/CartTodo.kt new file mode 100644 index 0000000000..8bd0c44087 --- /dev/null +++ b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/CartTodo.kt @@ -0,0 +1,42 @@ +package com.example.jetsnack.ui.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.example.jetsnack.* + +@Composable +fun CartTodo(modifier: Modifier = Modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxSize() + .wrapContentSize() + .padding(24.dp) + ) { + Image( + painterResource(MppR.drawable.empty_state_search), + contentDescription = null + ) + Spacer(Modifier.height(24.dp)) + Text( + text = stringResource(MppR.string.work_in_progress), + style = MaterialTheme.typography.subtitle1, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(16.dp)) + Text( + text = stringResource(MppR.string.grab_beverage), + style = MaterialTheme.typography.body2, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/SnackDialog.kt b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/SnackDialog.kt new file mode 100644 index 0000000000..9d9255cfbc --- /dev/null +++ b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/SnackDialog.kt @@ -0,0 +1,9 @@ +package com.example.jetsnack.ui.home + +import androidx.compose.runtime.Composable +import androidx.compose.ui.window.Popup + +@Composable +actual fun SnackDialog(onCloseRequest: () -> Unit, content: @Composable () -> Unit) { + Popup(onDismissRequest = onCloseRequest, content = { content() }) +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.nonAndroid.kt b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.nonAndroid.kt new file mode 100644 index 0000000000..83b50732b9 --- /dev/null +++ b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/cart/Cart.nonAndroid.kt @@ -0,0 +1,112 @@ +package com.example.jetsnack.ui.home.cart + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.example.jetsnack.MppR +import com.example.jetsnack.label_remove +import com.example.jetsnack.model.OrderLine +import com.example.jetsnack.model.SnackRepo +import com.example.jetsnack.model.SnackbarManager +import com.example.jetsnack.pluralsLocal +import com.example.jetsnack.stringResource +import com.example.jetsnack.ui.components.QuantitySelector +import com.example.jetsnack.ui.components.SnackImage +import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.utils.formatPrice + +@Composable +actual fun rememberQuantityString(res: Int, qty: Int, vararg args: Any): String { + val plurals = pluralsLocal.current + + return remember(res, qty, plurals) { + var str = plurals[res]?.forQuantity(qty) ?: "" + args.forEachIndexed { index, any -> + str = str.replace("%${index + 1}d", any.toString()) + } + str + } +} + +@Composable +actual fun ActualCartItem( + orderLine: OrderLine, + removeSnack: (Long) -> Unit, + increaseItemCount: (Long) -> Unit, + decreaseItemCount: (Long) -> Unit, + onSnackClick: (Long) -> Unit, + modifier: Modifier +) { + val snack = orderLine.snack + + Row(modifier = modifier + .fillMaxWidth() + .clickable { onSnackClick(snack.id) } + .background(JetsnackTheme.colors.uiBackground) + .padding(horizontal = 24.dp) + ) { + SnackImage( + imageUrl = snack.imageUrl, + contentDescription = null, + modifier = Modifier.padding(top = 4.dp).size(100.dp) + ) + Column(modifier = Modifier.padding(12.dp).weight(1f)) { + Text( + text = snack.name, + style = MaterialTheme.typography.subtitle1, + color = JetsnackTheme.colors.textSecondary, + ) + Text( + text = snack.tagline, + style = MaterialTheme.typography.body1, + color = JetsnackTheme.colors.textHelp, + ) + Text( + text = formatPrice(snack.price), + style = MaterialTheme.typography.subtitle1, + color = JetsnackTheme.colors.textPrimary, + modifier = Modifier.padding(top = 8.dp) + ) + } + Column(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.End) { + IconButton( + onClick = { removeSnack(snack.id) }, + modifier = Modifier.padding(top = 12.dp) + ) { + Icon( + imageVector = Icons.Filled.Close, + tint = JetsnackTheme.colors.iconSecondary, + contentDescription = stringResource(MppR.string.label_remove) + ) + } + QuantitySelector( + count = orderLine.count, + decreaseItemCount = { decreaseItemCount(snack.id) }, + increaseItemCount = { increaseItemCount(snack.id) }, + modifier = Modifier.padding(top = 12.dp) + ) + } + } + +} + +@Composable +actual fun getCartContentInsets(): WindowInsets { + return WindowInsets(top = 56.dp) +} + +@Composable +actual fun provideCartViewModel(): CartViewModel { + return remember { CartViewModel(SnackbarManager, SnackRepo) } +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.nonAndroid.kt b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.nonAndroid.kt new file mode 100644 index 0000000000..780471f078 --- /dev/null +++ b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/cart/CartViewModel.nonAndroid.kt @@ -0,0 +1,15 @@ +package com.example.jetsnack.ui.home.cart + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import com.example.jetsnack.model.OrderLine +import kotlinx.coroutines.flow.StateFlow + +actual abstract class JetSnackCartViewModel actual constructor() { + + @Composable + actual fun collectOrderLinesAsState(flow: StateFlow>): State> { + return flow.collectAsState() + } +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/snackCollectionListItemWindowInsets.kt b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/snackCollectionListItemWindowInsets.kt new file mode 100644 index 0000000000..0db2843f9b --- /dev/null +++ b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/home/snackCollectionListItemWindowInsets.kt @@ -0,0 +1,12 @@ +package com.example.jetsnack.ui.home + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp + +@Composable +actual fun snackCollectionListItemWindowInsets(): WindowInsets { + // TODO: implement + // WindowInsets.statusBars.add(WindowInsets(top = 56.dp)) + return WindowInsets(top = 56.dp) +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/myiconpack/EmptyStateSearch.kt b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/myiconpack/EmptyStateSearch.kt new file mode 100644 index 0000000000..b769061405 --- /dev/null +++ b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/myiconpack/EmptyStateSearch.kt @@ -0,0 +1,124 @@ +package com.example.jetsnack.ui.myiconpack + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +public val EmptyStateSearch: ImageVector + get() { + if (_emptyStateSearch != null) { + return _emptyStateSearch!! + } + _emptyStateSearch = Builder(name = "EmptyStateSearch", defaultWidth = 341.0.dp, + defaultHeight = 179.0.dp, viewportWidth = 341.0f, viewportHeight = 179.0f).apply { + path(fill = SolidColor(Color(0xFFDDE3E8)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero) { + moveTo(302.676f, 111.056f) + lineTo(244.424f, 65.728f) + curveTo(234.123f, 57.654f, 224.238f, 49.061f, 214.807f, 39.98f) + curveTo(198.202f, 24.102f, 175.659f, 11.407f, 149.414f, 4.648f) + curveTo(85.649f, -11.772f, 35.135f, 17.344f, 12.16f, 60.096f) + curveTo(-22.949f, 125.426f, 20.921f, 195.341f, 105.817f, 175.009f) + curveTo(145.621f, 169.5f, 174.324f, 161.356f, 200.455f, 154.855f) + lineTo(295.072f, 135.285f) + lineTo(302.676f, 111.056f) + close() + } + path(fill = SolidColor(Color(0xFFffffff)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero) { + moveTo(288.225f, 120.035f) + arcToRelative(12.46f, 10.541f, 105.0f, true, false, 20.363f, 5.456f) + arcToRelative(12.46f, 10.541f, 105.0f, true, false, -20.363f, -5.456f) + close() + } + path(fill = SolidColor(Color(0xFF3C4043)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero) { + moveTo(299.659f, 110.277f) + curveTo(304.701f, 111.618f, 309.064f, 114.797f, 311.893f, 119.193f) + lineTo(313.356f, 121.465f) + lineTo(313.43f, 121.559f) + lineTo(339.097f, 129.093f) + curveTo(339.567f, 129.232f, 339.965f, 129.549f, 340.204f, 129.979f) + curveTo(340.444f, 130.408f, 340.505f, 130.914f, 340.376f, 131.389f) + lineTo(338.384f, 138.718f) + curveTo(338.319f, 138.957f, 338.208f, 139.18f, 338.056f, 139.376f) + curveTo(337.905f, 139.571f, 337.716f, 139.734f, 337.502f, 139.856f) + curveTo(337.287f, 139.979f, 337.051f, 140.057f, 336.806f, 140.087f) + curveTo(336.561f, 140.117f, 336.313f, 140.098f, 336.075f, 140.032f) + lineTo(310.402f, 132.833f) + lineTo(310.401f, 132.834f) + lineTo(307.823f, 133.812f) + curveTo(303.075f, 135.612f, 297.867f, 135.79f, 293.008f, 134.317f) + verticalLineTo(134.317f) + lineTo(299.659f, 110.277f) + close() + } + path(fill = SolidColor(Color(0xFF3C4043)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = EvenOdd) { + moveTo(161.472f, 52.165f) + lineTo(151.381f, 69.821f) + verticalLineTo(69.849f) + curveTo(160.101f, 74.643f, 167.496f, 81.558f, 172.896f, 89.966f) + curveTo(178.297f, 98.374f, 181.531f, 108.01f, 182.306f, 118.0f) + horizontalLineTo(61.0f) + curveTo(61.765f, 108.002f, 64.996f, 98.356f, 70.397f, 89.939f) + curveTo(75.798f, 81.523f, 83.198f, 74.602f, 91.925f, 69.807f) + lineTo(81.827f, 52.165f) + curveTo(81.551f, 51.678f, 81.478f, 51.101f, 81.624f, 50.56f) + curveTo(81.77f, 50.019f, 82.122f, 49.558f, 82.605f, 49.279f) + curveTo(83.087f, 49.001f, 83.659f, 48.927f, 84.195f, 49.074f) + curveTo(84.731f, 49.221f, 85.188f, 49.577f, 85.464f, 50.064f) + lineTo(95.687f, 67.93f) + curveTo(103.852f, 64.232f, 112.7f, 62.321f, 121.65f, 62.321f) + curveTo(130.599f, 62.321f, 139.448f, 64.232f, 147.613f, 67.93f) + lineTo(157.836f, 50.064f) + curveTo(158.112f, 49.577f, 158.568f, 49.221f, 159.104f, 49.074f) + curveTo(159.64f, 48.927f, 160.213f, 49.001f, 160.695f, 49.279f) + curveTo(161.177f, 49.558f, 161.53f, 50.019f, 161.676f, 50.56f) + curveTo(161.822f, 51.101f, 161.748f, 51.678f, 161.472f, 52.165f) + close() + moveTo(133.338f, 84.859f) + curveTo(133.338f, 79.463f, 128.696f, 75.709f, 121.95f, 75.709f) + curveTo(116.815f, 75.709f, 113.167f, 77.774f, 111.438f, 81.052f) + curveTo(110.345f, 83.124f, 111.889f, 85.617f, 114.226f, 85.617f) + curveTo(114.833f, 85.623f, 115.428f, 85.455f, 115.943f, 85.133f) + curveTo(116.457f, 84.81f, 116.869f, 84.346f, 117.129f, 83.797f) + curveTo(117.868f, 82.172f, 119.481f, 81.177f, 121.518f, 81.177f) + curveTo(124.199f, 81.177f, 126.358f, 82.82f, 126.358f, 85.058f) + curveTo(126.358f, 87.296f, 125.08f, 88.451f, 122.04f, 90.273f) + curveTo(118.783f, 92.186f, 117.488f, 94.496f, 117.794f, 98.214f) + lineTo(117.8f, 98.39f) + curveTo(117.813f, 98.76f, 117.968f, 99.109f, 118.233f, 99.366f) + curveTo(118.498f, 99.623f, 118.852f, 99.766f, 119.22f, 99.766f) + horizontalLineTo(122.669f) + curveTo(122.856f, 99.766f, 123.041f, 99.729f, 123.213f, 99.658f) + curveTo(123.386f, 99.586f, 123.543f, 99.481f, 123.675f, 99.349f) + curveTo(123.806f, 99.216f, 123.911f, 99.059f, 123.983f, 98.886f) + curveTo(124.054f, 98.713f, 124.091f, 98.528f, 124.091f, 98.341f) + curveTo(124.091f, 96.031f, 125.152f, 94.695f, 128.283f, 92.872f) + curveTo(131.611f, 90.905f, 133.338f, 88.433f, 133.338f, 84.859f) + close() + moveTo(121.068f, 102.925f) + curveTo(118.945f, 102.925f, 117.218f, 104.567f, 117.218f, 106.642f) + curveTo(117.218f, 108.736f, 118.927f, 110.36f, 121.068f, 110.36f) + curveTo(123.209f, 110.36f, 124.936f, 108.736f, 124.936f, 106.642f) + curveTo(124.936f, 104.549f, 123.209f, 102.925f, 121.068f, 102.925f) + close() + } + } + .build() + return _emptyStateSearch!! + } + +private var _emptyStateSearch: ImageVector? = null diff --git a/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/snackdetail/jetSnackNavigationBarsPadding.kt b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/snackdetail/jetSnackNavigationBarsPadding.kt new file mode 100644 index 0000000000..4d61b581f0 --- /dev/null +++ b/examples/jetsnack/common/src/nonAndroidMain/kotlin/com/example/jetsnack/ui/snackdetail/jetSnackNavigationBarsPadding.kt @@ -0,0 +1,11 @@ +package com.example.jetsnack.ui.snackdetail + +import androidx.compose.ui.Modifier + +actual fun Modifier.jetSnackNavigationBarsPadding(): Modifier = this +// .navigationBarsPadding() + +actual fun Modifier.jetSnackStatusBarsPadding(): Modifier = this +// statusBarsPadding() +actual fun Modifier.jetSnackSystemBarsPadding(): Modifier = this +// Modifier.systemBarsPadding() \ No newline at end of file diff --git a/examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/model/createRandomUUID.kt b/examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/model/createRandomUUID.kt new file mode 100644 index 0000000000..d52e9bed73 --- /dev/null +++ b/examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/model/createRandomUUID.kt @@ -0,0 +1,8 @@ +package com.example.jetsnack.model + +import org.jetbrains.skiko.currentNanoTime + +actual fun createRandomUUID(): Long { + // TODO: implement. Create random UUID + return currentNanoTime() +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/ui/components/ImageLoader.kt b/examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/ui/components/ImageLoader.kt new file mode 100644 index 0000000000..58d5112d68 --- /dev/null +++ b/examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/ui/components/ImageLoader.kt @@ -0,0 +1,66 @@ +package com.example.jetsnack.ui.components + +import org.jetbrains.skia.ExternalSymbolName +import org.jetbrains.skia.impl.NativePointer +import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.Int8Array +import org.w3c.xhr.XMLHttpRequest +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import kotlin.wasm.unsafe.UnsafeWasmMemoryApi +import kotlin.wasm.unsafe.withScopedMemoryAllocator + +private class MissingResourceException(url: String): Exception("GET $url failed") + +suspend fun loadImage(url: String): ArrayBuffer { + return suspendCoroutine { continuation -> + val req = XMLHttpRequest() + req.open("GET", url, true) + req.responseType = "arraybuffer".toJsString().unsafeCast() + + req.onload = { _ -> + val arrayBuffer = req.response + if (arrayBuffer is ArrayBuffer) { + continuation.resume(arrayBuffer) + } else { + continuation.resumeWithException(MissingResourceException(url)) + } + } + req.send("") + } +} + +fun ArrayBuffer.toByteArray(): ByteArray { + val source = Int8Array(this, 0, byteLength) + return jsInt8ArrayToKotlinByteArray(source) +} + +@JsFun( + """ (src, size, dstAddr) => { + const mem8 = new Int8Array(wasmExports.memory.buffer, dstAddr, size); + mem8.set(src); + } +""" +) +internal external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr: Int) + +internal fun jsInt8ArrayToKotlinByteArray(x: Int8Array): ByteArray { + val size = x.length + + @OptIn(UnsafeWasmMemoryApi::class) + return withScopedMemoryAllocator { allocator -> + val memBuffer = allocator.allocate(size) + val dstAddress = memBuffer.address.toInt() + jsExportInt8ArrayToWasm(x, size, dstAddress) + ByteArray(size) { i -> (memBuffer + i).loadByte() } + } +} + +@ExternalSymbolName("_malloc") +@kotlin.wasm.WasmImport("skia", "malloc") +private external fun _malloc(size: Int): NativePointer + +@ExternalSymbolName("_free") +@kotlin.wasm.WasmImport("skia", "free") +private external fun _free(ptr: NativePointer) \ No newline at end of file diff --git a/examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt b/examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt new file mode 100644 index 0000000000..c06206cae7 --- /dev/null +++ b/examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/ui/components/SnackAsyncImage.kt @@ -0,0 +1,50 @@ +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 kotlinx.coroutines.* +import com.example.jetsnack.model.snacks + +val imagesCache = mutableMapOf() + +@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 { + val arrayBuffer = loadImage(imageUrl) + val skiaImg = org.jetbrains.skia.Image.makeFromEncoded(arrayBuffer.toByteArray()) + imagesCache[imageUrl] = skiaImg.toComposeImageBitmap() + bitmap = imagesCache[imageUrl] + } + } +} +suspend fun CoroutineScope.prepareImagesCache() { + val jobs = mutableListOf() + // We have not many images, so we can prepare and cache them upfront + snacks.forEach { + val j = launch { + val arrayBuffer = loadImage(it.imageUrl) + val skiaImg = org.jetbrains.skia.Image.makeFromEncoded(arrayBuffer.toByteArray()) + imagesCache[it.imageUrl] = skiaImg.toComposeImageBitmap() + } + jobs.add(j) + } + joinAll(*jobs.toTypedArray()) +} \ No newline at end of file diff --git a/examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.kt b/examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.kt new file mode 100644 index 0000000000..df9fee43f1 --- /dev/null +++ b/examples/jetsnack/common/src/wasmJsMain/kotlin/com/example/jetsnack/ui/utils/formatPrice.kt @@ -0,0 +1,17 @@ +package com.example.jetsnack.ui.utils + + +private external object Intl { + class NumberFormat(locales: String, options: JsAny) { + fun format(l: Double): String + } +} + +private fun formatAsUSD(): JsAny = js("({ style: 'currency', currency: 'USD',})") + +private val formatter = Intl.NumberFormat("en-US", formatAsUSD()) + +actual fun formatPrice(price: Long): String { + // price is represented in total amount of cents, so divide by 100 + return formatter.format(price / 100.0) +} \ No newline at end of file diff --git a/examples/jetsnack/desktop/build.gradle.kts b/examples/jetsnack/desktop/build.gradle.kts new file mode 100644 index 0000000000..8b3725f697 --- /dev/null +++ b/examples/jetsnack/desktop/build.gradle.kts @@ -0,0 +1,41 @@ +import org.jetbrains.compose.compose +import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("multiplatform") + id("org.jetbrains.compose") +} + +group = "com.example" +version = "1.0-SNAPSHOT" + + +kotlin { + jvm { + compilations.all { + kotlinOptions.jvmTarget = "17" + } + withJava() + } + sourceSets { + val jvmMain by getting { + dependencies { + implementation(project(":common")) + implementation(compose.desktop.currentOs) + } + } + val jvmTest by getting + } +} + +compose.desktop { + application { + mainClass = "MainKt" + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "jetsnak-mpp" + packageVersion = "1.0.0" + } + } +} diff --git a/examples/jetsnack/desktop/src/jvmMain/kotlin/Main.kt b/examples/jetsnack/desktop/src/jvmMain/kotlin/Main.kt new file mode 100644 index 0000000000..64d88fca98 --- /dev/null +++ b/examples/jetsnack/desktop/src/jvmMain/kotlin/Main.kt @@ -0,0 +1,15 @@ +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState +import com.example.jetsnack.JetSnackAppEntryPoint + + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + state = rememberWindowState(width = 1200.dp, height = 960.dp) + ) { + JetSnackAppEntryPoint() + } +} diff --git a/examples/jetsnack/gradle.properties b/examples/jetsnack/gradle.properties new file mode 100644 index 0000000000..a0e0f57cae --- /dev/null +++ b/examples/jetsnack/gradle.properties @@ -0,0 +1,29 @@ +org.gradle.jvmargs=-Xmx3g +kotlin.code.style=official +android.useAndroidX=true +agp.version=8.0.2 + +#Compose for Web is Experimental +org.jetbrains.compose.experimental.wasm.enabled=true + +# region Versions for Kotlin 1.9.x Generation +kotlin.version.1.9.x=1.9.23 +compose.compiler.version.1.9.x=1.5.8 +compose.wasm.version.1.9.x=1.6.0 + +# To enable K2 Kotlin compiler uncomment the next line +#kotlin.experimental.tryK2=true + +# endregion + +# region Versions for Kotlin 2.0.0 Generation +kotlin.version.2.0.0=2.0.0-Beta4 +compose.compiler.version.2.0.0=1.5.9-kt-2.0.0-Beta4 +compose.wasm.version.2.0.0=1.6.0 + + +# endregion + +# +kotlin.generation=1.9.x +#kotlin.generation=2.0.0 diff --git a/examples/jetsnack/gradle/libs.versions.toml b/examples/jetsnack/gradle/libs.versions.toml new file mode 100644 index 0000000000..3cddf4b819 --- /dev/null +++ b/examples/jetsnack/gradle/libs.versions.toml @@ -0,0 +1,138 @@ +##### +# This file is duplicated to individual samples from the global scripts/libs.versions.toml +# Do not add a dependency to an individual sample, edit the global version instead. +##### +[versions] +accompanist = "0.28.0" +androidGradlePlugin = "7.4.2" +androidx-activity-compose = "1.7.0" +androidx-appcompat = "1.6.1" +androidx-benchmark = "1.1.0" +androidx-benchmark-junit4 = "1.1.0-beta04" +androidx-compose-bom = "2023.03.00" +androidx-constraintlayout = "1.0.1" +androidx-corektx = "1.9.0" +androidx-lifecycle-compose = "2.6.1" +androidx-lifecycle-runtime-compose = "2.6.1" +androidx-navigation = "2.5.3" +androidx-palette = "1.0.0" +androidx-test = "1.5.0" +androidx-test-espresso = "3.5.1" +androidx-test-ext-junit = "1.1.5" +androidx-test-ext-truth = "1.4.0" +androidx-window = "1.1.0-beta01" +androidxHiltNavigationCompose = "1.0.0" +androix-test-uiautomator = "2.2.0" +coil = "2.2.0" +# @keep +compileSdk = "33" +compose-compiler = "1.6.2" +coroutines = "1.6.4" +google-maps = "18.1.0" +gradle-versions = "0.46.0" +hilt = "2.43.2" +hiltExt = "1.0.0" +# @pin When updating to AGP 7.4.0-alpha10 and up we can update this https://developer.android.com/studio/write/java8-support#library-desugaring-versions +jdkDesugar = "1.2.2" +junit = "4.13.2" +# @pin Update in conjuction with Compose Compiler +kotlin = "1.9.23" +ksp = "1.8.0-1.0.9" +maps-compose = "2.5.3" +material = "1.9.0-beta01" +# @keep +minSdk = "21" +okhttp = "4.10.0" +robolectric = "4.9.2" +rome = "1.18.0" +room = "2.5.0" +secrets = "2.0.1" +# @keep +targetSdk = "33" +version-catalog-update = "0.8.0" + +[libraries] +accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" } +accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" } +accompanist-pager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanist" } +accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } +accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanist" } +accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" } +androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity-compose" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } +androidx-benchmark-macrobenchmark = { module = "androidx.benchmark:benchmark-macro", version.ref = "androidx-benchmark" } +androidx-benchmark-macrobenchmark-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidx-benchmark-junit4" } +androidx-compose-animation = { module = "androidx.compose.animation:animation" } +androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } +androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } +androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } +androidx-compose-material = { module = "androidx.compose.material:material" } +androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" } +androidx-compose-material3 = { module = "androidx.compose.material3:material3" } +androidx-compose-materialWindow = { module = "androidx.compose.material3:material3-window-size-class" } +androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } +androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" } +androidx-compose-ui = { module = "androidx.compose.ui:ui" } +androidx-compose-ui-googlefonts = { module = "androidx.compose.ui:ui-text-google-fonts" } +androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test" } +androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } +androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +androidx-compose-ui-util = { module = "androidx.compose.ui:ui-util" } +androidx-compose-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding" } +androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "androidx-constraintlayout" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-corektx" } +androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } +androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } +androidx-lifecycle-runtime = "androidx.lifecycle:lifecycle-runtime-ktx:2.6.0-alpha04" +androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle-runtime-compose" } +androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle-compose" } +androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } +androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidx-lifecycle-compose" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } +androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation" } +androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" } +androidx-palette = { module = "androidx.palette:palette", version.ref = "androidx-palette" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } +androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso" } +androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" } +androidx-test-ext-truth = { module = "androidx.test.ext:truth", version.ref = "androidx-test-ext-truth" } +androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test" } +androidx-test-runner = "androidx.test:runner:1.5.2" +androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androix-test-uiautomator" } +androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" } +coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } +core-jdk-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "jdkDesugar" } +google-android-material = { module = "com.google.android.material:material", version.ref = "material" } +googlemaps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "maps-compose" } +googlemaps-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "google-maps" } +hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } +hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } +hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } +hilt-ext-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltExt" } +junit = { module = "junit:junit", version.ref = "junit" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } +okhttp3 = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +rometools-modules = { module = "com.rometools:rome-modules", version.ref = "rome" } +rometools-rome = { module = "com.rometools:rome", version.ref = "rome" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } +gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" } +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } +version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" } diff --git a/examples/jetsnack/gradle/wrapper/gradle-wrapper.jar b/examples/jetsnack/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..033e24c4cd Binary files /dev/null and b/examples/jetsnack/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/jetsnack/gradle/wrapper/gradle-wrapper.properties b/examples/jetsnack/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..8838ba97ba --- /dev/null +++ b/examples/jetsnack/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/jetsnack/gradlew b/examples/jetsnack/gradlew new file mode 100755 index 0000000000..fcb6fca147 --- /dev/null +++ b/examples/jetsnack/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/jetsnack/gradlew.bat b/examples/jetsnack/gradlew.bat new file mode 100644 index 0000000000..93e3f59f13 --- /dev/null +++ b/examples/jetsnack/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/jetsnack/ios/Configuration/Config.xcconfig b/examples/jetsnack/ios/Configuration/Config.xcconfig new file mode 100644 index 0000000000..6234dc9e5b --- /dev/null +++ b/examples/jetsnack/ios/Configuration/Config.xcconfig @@ -0,0 +1,3 @@ +TEAM_ID= +BUNDLE_ID=org.jetbrains.Imageviewer +APP_NAME=My Memories diff --git a/examples/jetsnack/ios/iosApp.xcodeproj/project.pbxproj b/examples/jetsnack/ios/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..b1b72a7956 --- /dev/null +++ b/examples/jetsnack/ios/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,390 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; + 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; + 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; + 3251B13A2AA8D738001521C0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3251B1392AA8D738001521C0 /* ContentView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; + 3251B1392AA8D738001521C0 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 7555FF7B242A565900829871 /* My Memories.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "My Memories.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + F85CB1118929364A9C6EFABC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 058557D7273AAEEB004C7B11 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 7555FF72242A565900829871 = { + isa = PBXGroup; + children = ( + AB1DB47929225F7C00F7AF9C /* Configuration */, + 7555FF7D242A565900829871 /* iosApp */, + 7555FF7C242A565900829871 /* Products */, + FEFF387C0A8D172AA4D59CAE /* Pods */, + ); + sourceTree = ""; + }; + 7555FF7C242A565900829871 /* Products */ = { + isa = PBXGroup; + children = ( + 7555FF7B242A565900829871 /* My Memories.app */, + ); + name = Products; + sourceTree = ""; + }; + 7555FF7D242A565900829871 /* iosApp */ = { + isa = PBXGroup; + children = ( + 058557BA273AAA24004C7B11 /* Assets.xcassets */, + 7555FF8C242A565B00829871 /* Info.plist */, + 2152FB032600AC8F00CF470E /* iOSApp.swift */, + 058557D7273AAEEB004C7B11 /* Preview Content */, + 3251B1392AA8D738001521C0 /* ContentView.swift */, + ); + path = iosApp; + sourceTree = ""; + }; + AB1DB47929225F7C00F7AF9C /* Configuration */ = { + isa = PBXGroup; + children = ( + AB3632DC29227652001CCB65 /* Config.xcconfig */, + ); + path = Configuration; + sourceTree = ""; + }; + FEFF387C0A8D172AA4D59CAE /* Pods */ = { + isa = PBXGroup; + children = ( + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7555FF7A242A565900829871 /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + 0548D1B72A8FD1EA002E7F63 /* Compile Kotlin */, + 7555FF77242A565900829871 /* Sources */, + 7555FF79242A565900829871 /* Resources */, + F85CB1118929364A9C6EFABC /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iosApp; + productName = iosApp; + productReference = 7555FF7B242A565900829871 /* My Memories.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7555FF73242A565900829871 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1130; + LastUpgradeCheck = 1130; + ORGANIZATIONNAME = orgName; + TargetAttributes = { + 7555FF7A242A565900829871 = { + CreatedOnToolsVersion = 11.3.1; + }; + }; + }; + buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7555FF72242A565900829871; + productRefGroup = 7555FF7C242A565900829871 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7555FF7A242A565900829871 /* iosApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7555FF79242A565900829871 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, + 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0548D1B72A8FD1EA002E7F63 /* Compile Kotlin */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Compile Kotlin"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd \"$SRCROOT/..\"\n./gradlew :common:embedAndSignAppleFrameworkForXcode\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7555FF77242A565900829871 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3251B13A2AA8D738001521C0 /* ContentView.swift in Sources */, + 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 7555FFA3242A565B00829871 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7555FFA4242A565B00829871 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7555FFA6242A565B00829871 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../common/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n"; + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + "common\n", + ); + PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; + PRODUCT_NAME = "${APP_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7555FFA7242A565B00829871 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../common/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n"; + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + "common\n", + ); + PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; + PRODUCT_NAME = "${APP_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7555FFA3242A565B00829871 /* Debug */, + 7555FFA4242A565B00829871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7555FFA6242A565B00829871 /* Debug */, + 7555FFA7242A565B00829871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7555FF73242A565900829871 /* Project object */; +} diff --git a/examples/jetsnack/ios/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/jetsnack/ios/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/examples/jetsnack/ios/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/jetsnack/ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/Artem.Kobzar.xcuserdatad/UserInterfaceState.xcuserstate b/examples/jetsnack/ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/Artem.Kobzar.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000..3a2cd48ad9 Binary files /dev/null and b/examples/jetsnack/ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/Artem.Kobzar.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/examples/jetsnack/ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/Artem.Kobzar.xcuserdatad/xcschemes/xcschememanagement.plist b/examples/jetsnack/ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/Artem.Kobzar.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..ee3458dd76 --- /dev/null +++ b/examples/jetsnack/ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/Artem.Kobzar.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/jetsnack/ios/iosApp.xcodeproj/xcuserdata/Artem.Kobzar.xcuserdatad/xcschemes/iosApp.xcscheme b/examples/jetsnack/ios/iosApp.xcodeproj/xcuserdata/Artem.Kobzar.xcuserdatad/xcschemes/iosApp.xcscheme new file mode 100644 index 0000000000..8734c1fb9d --- /dev/null +++ b/examples/jetsnack/ios/iosApp.xcodeproj/xcuserdata/Artem.Kobzar.xcuserdatad/xcschemes/iosApp.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/jetsnack/ios/iosApp.xcodeproj/xcuserdata/Artem.Kobzar.xcuserdatad/xcschemes/xcschememanagement.plist b/examples/jetsnack/ios/iosApp.xcodeproj/xcuserdata/Artem.Kobzar.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..fa59f97d5e --- /dev/null +++ b/examples/jetsnack/ios/iosApp.xcodeproj/xcuserdata/Artem.Kobzar.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + iosApp.xcscheme + + orderHint + 0 + + + + diff --git a/examples/jetsnack/ios/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/jetsnack/ios/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..ee7e3ca03f --- /dev/null +++ b/examples/jetsnack/ios/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/examples/jetsnack/ios/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/jetsnack/ios/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..b79abec6de --- /dev/null +++ b/examples/jetsnack/ios/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "image_viewer_icon.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/jetsnack/ios/iosApp/Assets.xcassets/AppIcon.appiconset/image_viewer_icon.png b/examples/jetsnack/ios/iosApp/Assets.xcassets/AppIcon.appiconset/image_viewer_icon.png new file mode 100644 index 0000000000..4e44017d38 Binary files /dev/null and b/examples/jetsnack/ios/iosApp/Assets.xcassets/AppIcon.appiconset/image_viewer_icon.png differ diff --git a/examples/jetsnack/ios/iosApp/Assets.xcassets/Contents.json b/examples/jetsnack/ios/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..4aa7c5350b --- /dev/null +++ b/examples/jetsnack/ios/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/examples/jetsnack/ios/iosApp/ContentView.swift b/examples/jetsnack/ios/iosApp/ContentView.swift new file mode 100644 index 0000000000..f4a124a8ea --- /dev/null +++ b/examples/jetsnack/ios/iosApp/ContentView.swift @@ -0,0 +1,38 @@ +import UIKit +import SwiftUI +import common + +struct ContentView: View { + var body: some View { + ZStack { + ComposeView() + .ignoresSafeArea(.all) // Compose has own keyboard handler + VStack { + gradient.ignoresSafeArea(edges: .top).frame(height: 0) + Spacer() + } + }.preferredColorScheme(.dark) + } +} + +struct ComposeView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIViewController { + let controller = Main_iosKt.MainViewController() + controller.overrideUserInterfaceStyle = .light + return controller + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + } +} + +let gradient = LinearGradient( + colors: [ + Color.black.opacity(0.6), + Color.black.opacity(0.6), + Color.black.opacity(0.5), + Color.black.opacity(0.3), + Color.black.opacity(0.0), + ], + startPoint: .top, endPoint: .bottom +) diff --git a/examples/jetsnack/ios/iosApp/Info.plist b/examples/jetsnack/ios/iosApp/Info.plist new file mode 100644 index 0000000000..5b28bf5219 --- /dev/null +++ b/examples/jetsnack/ios/iosApp/Info.plist @@ -0,0 +1,52 @@ + + + + + NSLocationWhenInUseUsageDescription + This app uses location data to show taken photos on a map + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSCameraUsageDescription + This app uses camera for capturing photos + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/examples/jetsnack/ios/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/examples/jetsnack/ios/iosApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..4aa7c5350b --- /dev/null +++ b/examples/jetsnack/ios/iosApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/examples/jetsnack/ios/iosApp/iOSApp.swift b/examples/jetsnack/ios/iosApp/iOSApp.swift new file mode 100644 index 0000000000..927e0b97b8 --- /dev/null +++ b/examples/jetsnack/ios/iosApp/iOSApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct iOSApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/examples/jetsnack/kotlin-js-store/yarn.lock b/examples/jetsnack/kotlin-js-store/yarn.lock new file mode 100644 index 0000000000..8f1a300d6d --- /dev/null +++ b/examples/jetsnack/kotlin-js-store/yarn.lock @@ -0,0 +1,2794 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.3.tgz#8108265659d4c33e72ffe14e33d6cc5eb59f2fda" + integrity sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" + integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== + +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.9": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" + integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.3.5": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" + integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + +"@types/cors@^2.8.12": + version "2.8.13" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.13.tgz#b8ade22ba455a1b8cb3b5d3f35910fd204f84f94" + integrity sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA== + dependencies: + "@types/node" "*" + +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.37.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.37.0.tgz#29cebc6c2a3ac7fea7113207bf5a828fdf4d7ef1" + integrity sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" + integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": + version "4.17.43" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz#10d8444be560cb789c4735aea5eac6e5af45df54" + integrity sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*", "@types/express@^4.17.13": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/http-proxy@^1.17.8": + version "1.17.14" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.14.tgz#57f8ccaa1c1c3780644f8a94f9c6b5000b5e2eec" + integrity sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w== + dependencies: + "@types/node" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.8": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/mime@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45" + integrity sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*", "@types/node@>=10.0.0": + version "18.15.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" + integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== + +"@types/qs@*": + version "6.9.12" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.12.tgz#afa96b383a3a6fdc859453a1892d41b607fc7756" + integrity sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/retry@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.1": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" + integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== + dependencies: + "@types/express" "*" + +"@types/serve-static@*", "@types/serve-static@^1.13.10": + version "1.15.5" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.5.tgz#15e67500ec40789a1e8c9defc2d32a896f05b033" + integrity sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ== + dependencies: + "@types/http-errors" "*" + "@types/mime" "*" + "@types/node" "*" + +"@types/sockjs@^0.3.33": + version "0.3.36" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" + integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== + dependencies: + "@types/node" "*" + +"@types/ws@^8.5.1": + version "8.5.10" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" + integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== + dependencies: + "@types/node" "*" + +"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" + integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" + integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" + integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" + integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-opt" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/wast-printer" "1.11.6" + +"@webassemblyjs/wasm-gen@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" + integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" + integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + +"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" + integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" + integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^2.1.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" + integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== + +"@webpack-cli/info@^2.0.1": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" + integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== + +"@webpack-cli/serve@^2.0.3": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" + integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +abab@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-assertions@^1.7.6: + version "1.8.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" + integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== + +acorn@^8.7.1: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + +acorn@^8.8.2: + version "8.9.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59" + integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ== + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.9.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +body-parser@1.20.2, body-parser@^1.19.0: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.0.11: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" + integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +browserslist@^4.14.5: + version "4.21.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" + integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== + dependencies: + caniuse-lite "^1.0.30001449" + electron-to-chromium "^1.4.284" + node-releases "^2.0.8" + update-browserslist-db "^1.0.10" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001449: + version "1.0.30001481" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz#f58a717afe92f9e69d0e35ff64df596bfad93912" + integrity sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@3.5.3, chokidar@^3.5.1: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.10, colorette@^2.0.14: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +connect@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +cookie@~0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@~2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +custom-event@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" + integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg== + +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4.3.4, debug@^4.1.0, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +di@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" + integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +dom-serialize@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" + integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ== + dependencies: + custom-event "~1.0.0" + ent "~2.2.0" + extend "^3.0.0" + void-elements "^2.0.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.284: + version "1.4.368" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.368.tgz#75901f97d3e23da2e66feb1e61fbb8e70ac96430" + integrity sha512-e2aeCAixCj9M7nJxdB/wDjO6mbYX+lJJxSJCXDzlr5YPGYVofuJwGN9nKg2o6wWInjX6XmxRinn3AeJMK81ltw== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +engine.io-parser@~5.0.3: + version "5.0.6" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45" + integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== + +engine.io@~6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.4.1.tgz#8056b4526a88e779f9c280d820422d4e3eeaaae5" + integrity sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.4.1" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.0.3" + ws "~8.11.0" + +enhanced-resolve@^5.13.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +ent@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== + +envinfo@^7.7.3: + version "7.8.1" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" + integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== + +es-module-lexer@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" + integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +express@^4.17.3: + version "4.18.3" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.3.tgz#6870746f3ff904dee1819b82e4b51509afffb0d4" + integrity sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +follow-redirects@^1.0.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +format-util@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" + integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-monkey@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.5.tgz#fe450175f0db0d7ea758102e1d84096acb925788" + integrity sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" + integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.3, glob@^7.1.7: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-entities@^2.3.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" + integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-proxy-middleware@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.1.0.tgz#2119bc447ff8c257753b196fc5f1ce08a4cdf39f" + integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.11.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.0.tgz#36ad62f6f73c8253fd6472517a12483cf03e7ec4" + integrity sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ== + dependencies: + has "^1.0.3" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isbinaryfile@^4.0.8: + version "4.0.10" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" + integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +js-yaml@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +karma-chrome-launcher@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" + integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== + dependencies: + which "^1.2.1" + +karma-mocha@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d" + integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ== + dependencies: + minimist "^1.2.3" + +karma-sourcemap-loader@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488" + integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA== + dependencies: + graceful-fs "^4.2.10" + +karma-webpack@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-5.0.0.tgz#2a2c7b80163fe7ffd1010f83f5507f95ef39f840" + integrity sha512-+54i/cd3/piZuP3dr54+NcFeKOPnys5QeM1IY+0SPASwrtHsliXUiCL50iW+K9WWA7RvamC4macvvQ86l3KtaA== + dependencies: + glob "^7.1.3" + minimatch "^3.0.4" + webpack-merge "^4.1.5" + +karma@6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.2.tgz#a983f874cee6f35990c4b2dcc3d274653714de8e" + integrity sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ== + dependencies: + "@colors/colors" "1.5.0" + body-parser "^1.19.0" + braces "^3.0.2" + chokidar "^3.5.1" + connect "^3.7.0" + di "^0.0.1" + dom-serialize "^2.2.1" + glob "^7.1.7" + graceful-fs "^4.2.6" + http-proxy "^1.18.1" + isbinaryfile "^4.0.8" + lodash "^4.17.21" + log4js "^6.4.1" + mime "^2.5.2" + minimatch "^3.0.4" + mkdirp "^0.5.5" + qjobs "^1.2.0" + range-parser "^1.2.1" + rimraf "^3.0.2" + socket.io "^4.4.1" + source-map "^0.6.1" + tmp "^0.2.1" + ua-parser-js "^0.7.30" + yargs "^16.1.1" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +launch-editor@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.1.tgz#f259c9ef95cbc9425620bbbd14b468fcdb4ffe3c" + integrity sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.8.1" + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash@^4.17.15, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log4js@^6.4.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" + integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + flatted "^3.2.7" + rfdc "^1.3.0" + streamroller "^3.1.5" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^3.4.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" + integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== + dependencies: + fs-monkey "^1.0.4" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.2: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.5.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.3, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.5: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mocha@10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" + integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.4" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" + ms "2.1.3" + nanoid "3.3.3" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + workerpool "6.2.1" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-releases@^2.0.8: + version "2.0.10" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" + integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.9.0: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^8.0.9: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-retry@^4.5.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== + dependencies: + "@types/retry" "0.12.0" + retry "^0.13.1" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +qjobs@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" + integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@^2.0.1: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.20.0: + version "1.22.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" + integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== + dependencies: + is-core-module "^2.11.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + +rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.2.tgz#36c10abca6f7577aeae136c804b0c741edeadc99" + integrity sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^3.1.2: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" + integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.1.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" + integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +socket.io-adapter@~2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12" + integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA== + dependencies: + ws "~8.11.0" + +socket.io-parser@~4.2.1: + version "4.2.2" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206" + integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@^4.4.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.6.1.tgz#62ec117e5fce0692fa50498da9347cfb52c3bc70" + integrity sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + debug "~4.3.2" + engine.io "~6.4.1" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.1" + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map-loader@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-4.0.1.tgz#72f00d05f5d1f90f80974eda781cbd7107c125f2" + integrity sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA== + dependencies: + abab "^2.0.6" + iconv-lite "^0.6.3" + source-map-js "^1.0.2" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +streamroller@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" + integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + fs-extra "^8.1.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@8.1.1, supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.3.7: + version "5.3.9" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" + integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.17" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.16.8" + +terser@^5.16.8: + version "5.18.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.18.1.tgz#6d8642508ae9fb7b48768e48f16d675c89a78460" + integrity sha512-j1n0Ao919h/Ai5r43VAnfV/7azUYW43GPxK7qSATzrsERfW7+y2QW9Cp9ufnRF5CQUWbnLSo7UJokSWCqg4tsQ== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +tmp@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + +ua-parser-js@^0.7.30: + version "0.7.35" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.35.tgz#8bda4827be4f0b1dda91699a29499575a1f1d307" + integrity sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g== + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.0.10: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webpack-cli@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.0.tgz#abc4b1f44b50250f2632d8b8b536cfe2f6257891" + integrity sha512-a7KRJnCxejFoDpYTOwzm5o21ZXMaNqtRlvS183XzGDUPRdVEzJNImcQokqYZ8BNTnk9DkKiuWxw75+DCCoZ26w== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^2.1.0" + "@webpack-cli/info" "^2.0.1" + "@webpack-cli/serve" "^2.0.3" + colorette "^2.0.14" + commander "^10.0.1" + cross-spawn "^7.0.3" + envinfo "^7.7.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^3.1.1" + rechoir "^0.8.0" + webpack-merge "^5.7.3" + +webpack-dev-middleware@^5.3.1: + version "5.3.3" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" + integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== + dependencies: + colorette "^2.0.10" + memfs "^3.4.3" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@4.15.0: + version "4.15.0" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.0.tgz#87ba9006eca53c551607ea0d663f4ae88be7af21" + integrity sha512-HmNB5QeSl1KpulTBQ8UT4FPrByYyaLxpJoQ0+s7EvUrMc16m0ZS1sgb1XGqzmgCPk0c9y+aaXxn11tbLzuM7NQ== + dependencies: + "@types/bonjour" "^3.5.9" + "@types/connect-history-api-fallback" "^1.3.5" + "@types/express" "^4.17.13" + "@types/serve-index" "^1.9.1" + "@types/serve-static" "^1.13.10" + "@types/sockjs" "^0.3.33" + "@types/ws" "^8.5.1" + ansi-html-community "^0.0.8" + bonjour-service "^1.0.11" + chokidar "^3.5.3" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + default-gateway "^6.0.3" + express "^4.17.3" + graceful-fs "^4.2.6" + html-entities "^2.3.2" + http-proxy-middleware "^2.0.3" + ipaddr.js "^2.0.1" + launch-editor "^2.6.0" + open "^8.0.9" + p-retry "^4.5.0" + rimraf "^3.0.2" + schema-utils "^4.0.0" + selfsigned "^2.1.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^5.3.1" + ws "^8.13.0" + +webpack-merge@^4.1.5: + version "4.2.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" + integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== + dependencies: + lodash "^4.17.15" + +webpack-merge@^5.7.3: + version "5.8.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61" + integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== + dependencies: + clone-deep "^4.0.1" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@5.82.0: + version "5.82.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.82.0.tgz#3c0d074dec79401db026b4ba0fb23d6333f88e7d" + integrity sha512-iGNA2fHhnDcV1bONdUu554eZx+XeldsaeQ8T67H6KKHl2nUSwX8Zm7cmzOA46ox/X1ARxf7Bjv8wQ/HsB5fxBg== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" + acorn "^8.7.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.13.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.2" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.7" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +which@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" + integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== + +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^8.13.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0, yargs@^16.1.1: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/examples/jetsnack/screenshots/jetsnack.png b/examples/jetsnack/screenshots/jetsnack.png new file mode 100644 index 0000000000..517222d87b Binary files /dev/null and b/examples/jetsnack/screenshots/jetsnack.png differ diff --git a/examples/jetsnack/settings.gradle.kts b/examples/jetsnack/settings.gradle.kts new file mode 100644 index 0000000000..42d3f0abeb --- /dev/null +++ b/examples/jetsnack/settings.gradle.kts @@ -0,0 +1,20 @@ +pluginManagement { + repositories { + google() + gradlePluginPortal() + mavenCentral() + } + + plugins { + val kotlinGeneration = extra["kotlin.generation"] + kotlin("multiplatform").version(extra["kotlin.version.$kotlinGeneration"] as String) + kotlin("android").version(extra["kotlin.version.$kotlinGeneration"] as String) + id("com.android.application").version(extra["agp.version"] as String) + id("com.android.library").version(extra["agp.version"] as String) + id("org.jetbrains.compose").version(extra["compose.wasm.version.$kotlinGeneration"] as String) + } +} + +rootProject.name = "compose-jetsnack" + +include(":common", ":android", ":desktop", ":web") diff --git a/examples/jetsnack/web/build.gradle.kts b/examples/jetsnack/web/build.gradle.kts new file mode 100644 index 0000000000..9b0a24a92e --- /dev/null +++ b/examples/jetsnack/web/build.gradle.kts @@ -0,0 +1,51 @@ +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig + +plugins { + kotlin("multiplatform") + id("org.jetbrains.compose") +} + +group = "com.example" +version = "1.0-SNAPSHOT" + +kotlin { + @OptIn(org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl::class) + wasmJs { + moduleName = "jetsnackwasmapp" + browser { + commonWebpackConfig { + outputFileName = "jetsnackwasmapp.js" + devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply { + + static = (static ?: mutableListOf()).apply { + add(project.rootDir.path) + add(project.rootDir.path + "/common/") + add(project.rootDir.path + "/web/") + } + } + } + } + binaries.executable() + } + sourceSets { + val commonMain by getting { + dependencies { + api(compose.runtime) + api(compose.foundation) + api(compose.material) + @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) + api(compose.components.resources) + implementation(project(":common")) + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + } +} + +compose.experimental { + web.application {} +} diff --git a/examples/jetsnack/web/src/wasmJsMain/kotlin/Main.kt b/examples/jetsnack/web/src/wasmJsMain/kotlin/Main.kt new file mode 100644 index 0000000000..38faff4ffd --- /dev/null +++ b/examples/jetsnack/web/src/wasmJsMain/kotlin/Main.kt @@ -0,0 +1,71 @@ +import com.example.jetsnack.ui.components.prepareImagesCache +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.platform.Font +import androidx.compose.ui.window.CanvasBasedWindow +import com.example.jetsnack.JetSnackAppEntryPoint +import com.example.jetsnack.ui.components.loadImage +import com.example.jetsnack.ui.components.toByteArray +import com.example.jetsnack.ui.theme.Karla +import com.example.jetsnack.ui.theme.Montserrat +import org.jetbrains.compose.resources.* + +@OptIn(ExperimentalComposeUiApi::class, ExperimentalResourceApi::class) +fun main() { + configureWebResources { + // same as default - this is not necessary to add here. It's here to show this feature + resourcePathMapping { path -> "./$path" } + } + CanvasBasedWindow("JetSnack", canvasElementId = "jetsnackCanvas") { + var loading: Boolean by remember { mutableStateOf(true) } + + if (loading) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } else { + JetSnackAppEntryPoint() + } + + LaunchedEffect(Unit) { + loadMontserratFont() + loadKarlaFont() + prepareImagesCache() + loading = false + } + } +} + + +private suspend fun loadMontserratFont() { + val regular = loadResource("montserrat_regular.ttf") + val medium = loadResource("montserrat_medium.ttf") + val light = loadResource("montserrat_light.ttf") + val semiBold = loadResource("montserrat_semibold.ttf") + + Montserrat = FontFamily( + Font(identity = "MontserratRegular", data = regular, weight = FontWeight.Normal), + Font(identity = "MontserratMedium", data = medium, weight = FontWeight.Medium), + Font(identity = "MontserratLight", data = light, weight = FontWeight.Light), + Font(identity = "MontserratSemiBold", data = semiBold, weight = FontWeight.SemiBold), + ) +} + +private suspend fun loadKarlaFont() { + val regular = loadResource("karla_regular.ttf") + val bold = loadResource("karla_bold.ttf") + + Karla = FontFamily( + Font(identity = "KarlaRegular", data = regular, weight = FontWeight.Normal), + Font(identity = "KarlaBold", data = bold, weight = FontWeight.Bold), + ) +} + + +internal suspend fun loadResource(path: String): ByteArray { + return loadImage(path).toByteArray() +} + diff --git a/examples/jetsnack/web/src/wasmJsMain/resources/index.html b/examples/jetsnack/web/src/wasmJsMain/resources/index.html new file mode 100644 index 0000000000..9f866093ae --- /dev/null +++ b/examples/jetsnack/web/src/wasmJsMain/resources/index.html @@ -0,0 +1,111 @@ + + + + + + JetSnack with Kotlin/Wasm + + + + + + + +
+ ⚠️ Please make sure that your runtime environment supports the latest version of Wasm GC and Exception-Handling proposals. + For more information, see https://kotl.in/wasm-help. +
+
+
    +
  • For Chrome and Chromium-based browsers (Edge, Brave etc.), it should just work since version 119.
  • +
  • For Firefox 120 it should just work.
  • +
  • For Firefox 119: +
      +
    1. Open about:config in the browser.
    2. +
    3. Enable javascript.options.wasm_gc.
    4. +
    5. Refresh this page.
    6. +
    +
  • +
+
+ + + + + + diff --git a/examples/jetsnack/web/src/wasmJsMain/resources/karla_bold.ttf b/examples/jetsnack/web/src/wasmJsMain/resources/karla_bold.ttf new file mode 100644 index 0000000000..052231c165 Binary files /dev/null and b/examples/jetsnack/web/src/wasmJsMain/resources/karla_bold.ttf differ diff --git a/examples/jetsnack/web/src/wasmJsMain/resources/karla_regular.ttf b/examples/jetsnack/web/src/wasmJsMain/resources/karla_regular.ttf new file mode 100644 index 0000000000..4269aa069e Binary files /dev/null and b/examples/jetsnack/web/src/wasmJsMain/resources/karla_regular.ttf differ diff --git a/examples/jetsnack/web/src/wasmJsMain/resources/load.mjs b/examples/jetsnack/web/src/wasmJsMain/resources/load.mjs new file mode 100644 index 0000000000..67510ce7da --- /dev/null +++ b/examples/jetsnack/web/src/wasmJsMain/resources/load.mjs @@ -0,0 +1,5 @@ +import { instantiate } from './jetsnackwasmapp.uninstantiated.mjs'; + +await wasmSetup; + +instantiate({ skia: Module['asm'] }); diff --git a/examples/jetsnack/web/src/wasmJsMain/resources/montserrat_light.ttf b/examples/jetsnack/web/src/wasmJsMain/resources/montserrat_light.ttf new file mode 100644 index 0000000000..990857de8e Binary files /dev/null and b/examples/jetsnack/web/src/wasmJsMain/resources/montserrat_light.ttf differ diff --git a/examples/jetsnack/web/src/wasmJsMain/resources/montserrat_medium.ttf b/examples/jetsnack/web/src/wasmJsMain/resources/montserrat_medium.ttf new file mode 100644 index 0000000000..6e079f6984 Binary files /dev/null and b/examples/jetsnack/web/src/wasmJsMain/resources/montserrat_medium.ttf differ diff --git a/examples/jetsnack/web/src/wasmJsMain/resources/montserrat_regular.ttf b/examples/jetsnack/web/src/wasmJsMain/resources/montserrat_regular.ttf new file mode 100644 index 0000000000..8d443d5d56 Binary files /dev/null and b/examples/jetsnack/web/src/wasmJsMain/resources/montserrat_regular.ttf differ diff --git a/examples/jetsnack/web/src/wasmJsMain/resources/montserrat_semibold.ttf b/examples/jetsnack/web/src/wasmJsMain/resources/montserrat_semibold.ttf new file mode 100644 index 0000000000..f8a43f2b20 Binary files /dev/null and b/examples/jetsnack/web/src/wasmJsMain/resources/montserrat_semibold.ttf differ diff --git a/examples/jetsnack/web/webpack.config.d/boilerplate.js b/examples/jetsnack/web/webpack.config.d/boilerplate.js new file mode 100644 index 0000000000..e88b7f938c --- /dev/null +++ b/examples/jetsnack/web/webpack.config.d/boilerplate.js @@ -0,0 +1,7 @@ +config.entry = { + main: [require('path').resolve(__dirname, "kotlin/load.mjs")] +}; + +config.resolve ?? (config.resolve = {}); +config.resolve.alias ?? (config.resolve.alias = {}); +config.resolve.alias.skia = false; diff --git a/examples/jetsnack/web/webpack.config.d/cleanupSourcemap.js b/examples/jetsnack/web/webpack.config.d/cleanupSourcemap.js new file mode 100644 index 0000000000..144d487278 --- /dev/null +++ b/examples/jetsnack/web/webpack.config.d/cleanupSourcemap.js @@ -0,0 +1,31 @@ +// Replace paths unavailable during compilation with `null`, so they will not be shown in devtools +; +(() => { + const fs = require("fs"); + const path = require("path"); + + const outDir = __dirname + "/kotlin/" + const projecName = path.basename(__dirname); + const mapFile = outDir + projecName + ".map" + + const sourcemap = JSON.parse(fs.readFileSync(mapFile)) + const sources = sourcemap["sources"] + srcLoop: for (let i in sources) { + const srcFilePath = sources[i]; + if (srcFilePath == null) continue; + + const srcFileCandidates = [ + outDir + srcFilePath, + outDir + srcFilePath.substring("../".length), + outDir + "../" + srcFilePath, + ]; + + for (let srcFile of srcFileCandidates) { + if (fs.existsSync(srcFile)) continue srcLoop; + } + + sources[i] = null; + } + + fs.writeFileSync(mapFile, JSON.stringify(sourcemap)); +})();