Add order history tab (#59)
* Move Compose theme files to presentation package
* Move catalog composables to ui package
* Update compose theme name, colors, and typography
* Add order history tab and fragments
* Create empty order history page
* Create order history list page
* Create order details page
* Create view model and pass pages into fragments
* Pull order history from database
* Set up dev mode
* Adjust LazyColumn content padding
* Change padding in landscape mode
* Overlap guitars instead of aligning to edges
* Remove unnecessary handler from viewmodel
* Extract string formatting logic to utils file
* Refactor use case name to be more descriptive
* Add tests for new use case
* Prompt user to check out order history instead of other stores in the tutorial
* Update list order and fix nav issue
* Update UI for better tablet experience
* Create "add to order" dialog
* Navigate to details when order is selected in single screen mode
* Make sure default selected order is the most recent order
* Update UI for better compatability with other devices
* Clean up history viewmodel
* Add comments
* Create WindowState extensions
* Split up placeholder text/image when spanned
* Add screenshots to readme
* Make add to order dialog scrollable
* Finalize image/box placement and size
* testing - add log statements to trace navigation
* Revert "testing - add log statements to trace navigation"
This reverts commit 794ae3b1d8
.
* Refactor showTwoPages to isDualMode
* Code cleanup/PR comments
* Update composable names for clarity
* Fix flashing bug when switching between orders in the list
This commit is contained in:
Родитель
b27b34168d
Коммит
76cf7dff05
|
@ -56,6 +56,11 @@ To learn how to load apps on the Surface Duo emulator, see the [documentation](h
|
|||
<img src="screenshots/dual_portrait_dev_mode.png" width=49% />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="screenshots/dual_portrait_history_light.png" width=49% />
|
||||
<img src="screenshots/dual_portrait_history.png" width=49% />
|
||||
</p>
|
||||
|
||||
## Social links
|
||||
|
||||
| Blog post | Video |
|
||||
|
|
|
@ -34,6 +34,9 @@ android {
|
|||
arguments += ["room.incremental": "true"]
|
||||
}
|
||||
}
|
||||
vectorDrawables {
|
||||
useSupportLibrary true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -94,6 +97,12 @@ android {
|
|||
|
||||
// This is the default but added it explicitly so we can change it more easily
|
||||
testBuildType "debug"
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -146,6 +155,7 @@ dependencies {
|
|||
implementation microsoftDependencies.windowState
|
||||
|
||||
implementation uiDependencies.lottie
|
||||
implementation uiDependencies.lottieCompose
|
||||
|
||||
implementation uiDependencies.glide
|
||||
kapt uiDependencies.glideAnnotationProcesor
|
||||
|
|
|
@ -34,18 +34,18 @@ class PreferenceManagerTest {
|
|||
fun shouldShowTutorialWhenValueIsDefault() {
|
||||
assertTrue(tutorialPrefManager.shouldShowLaunchTutorial())
|
||||
assertTrue(tutorialPrefManager.shouldShowDevModeTutorial())
|
||||
assertTrue(tutorialPrefManager.shouldShowStoresTutorial())
|
||||
assertTrue(tutorialPrefManager.shouldShowHistoryTutorial())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldNotShowTutorialWhenValueIsSetToFalse() {
|
||||
tutorialPrefManager.setShowLaunchTutorial(false)
|
||||
tutorialPrefManager.setShowDevModeTutorial(false)
|
||||
tutorialPrefManager.setShowStoresTutorial(false)
|
||||
tutorialPrefManager.setShowHistoryTutorial(false)
|
||||
|
||||
assertFalse(tutorialPrefManager.shouldShowLaunchTutorial())
|
||||
assertFalse(tutorialPrefManager.shouldShowDevModeTutorial())
|
||||
assertFalse(tutorialPrefManager.shouldShowStoresTutorial())
|
||||
assertFalse(tutorialPrefManager.shouldShowHistoryTutorial())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -60,9 +60,9 @@ class PreferenceManagerTest {
|
|||
tutorialPrefManager.setShowDevModeTutorial(true)
|
||||
assertFalse(tutorialPrefManager.shouldShowDevModeTutorial())
|
||||
|
||||
assertTrue(tutorialPrefManager.shouldShowStoresTutorial())
|
||||
tutorialPrefManager.setShowStoresTutorial(false)
|
||||
tutorialPrefManager.setShowStoresTutorial(true)
|
||||
assertFalse(tutorialPrefManager.shouldShowStoresTutorial())
|
||||
assertTrue(tutorialPrefManager.shouldShowHistoryTutorial())
|
||||
tutorialPrefManager.setShowHistoryTutorial(false)
|
||||
tutorialPrefManager.setShowHistoryTutorial(true)
|
||||
assertFalse(tutorialPrefManager.shouldShowHistoryTutorial())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ class OrderRepositoryTest {
|
|||
|
||||
private val orderWithItems = OrderWithItems(firstOrderEntity, mutableListOf(firstOrderItemEntity))
|
||||
private val orderWithoutItems = OrderWithItems(firstOrderEntity, mutableListOf())
|
||||
private val submittedOrderWithoutItems = OrderWithItems(firstSubmittedOrderEntity, mutableListOf())
|
||||
|
||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
private lateinit var database: AppDatabase
|
||||
|
@ -92,4 +93,21 @@ class OrderRepositoryTest {
|
|||
|
||||
assertThat(result, iz(orderWithItems))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getAllSubmittedOrders() = runBlocking {
|
||||
var result = orderRepo.getAllSubmittedOrders().getOrAwaitValue()
|
||||
|
||||
assertThat(result, iz(Matchers.empty()))
|
||||
|
||||
orderRepo.insert(firstOrderEntity)
|
||||
result = orderRepo.getAllSubmittedOrders().getOrAwaitValue()
|
||||
|
||||
assertThat(result, iz(Matchers.empty()))
|
||||
|
||||
orderRepo.insert(firstSubmittedOrderEntity)
|
||||
result = orderRepo.getAllSubmittedOrders().getOrAwaitValue()
|
||||
|
||||
assertThat(result, iz(listOf(submittedOrderWithoutItems)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,3 +27,5 @@ val firstOrderEntity = OrderEntity(
|
|||
4354,
|
||||
false
|
||||
)
|
||||
|
||||
val firstSubmittedOrderEntity = firstOrderEntity.copy(isSubmitted = true)
|
||||
|
|
|
@ -32,12 +32,12 @@ class PreferenceManager @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun shouldShowStoresTutorial() =
|
||||
sharedPref.getBoolean(TutorialPrefType.STORES.toString(), true)
|
||||
override fun shouldShowHistoryTutorial() =
|
||||
sharedPref.getBoolean(TutorialPrefType.HISTORY.toString(), true)
|
||||
|
||||
override fun setShowStoresTutorial(value: Boolean) {
|
||||
if (shouldShowStoresTutorial()) {
|
||||
sharedPref.setValue(TutorialPrefType.STORES.toString(), value)
|
||||
override fun setShowHistoryTutorial(value: Boolean) {
|
||||
if (shouldShowHistoryTutorial()) {
|
||||
sharedPref.setValue(TutorialPrefType.HISTORY.toString(), value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@ interface TutorialPreferences {
|
|||
fun setShowLaunchTutorial(value: Boolean)
|
||||
fun shouldShowDevModeTutorial(): Boolean
|
||||
fun setShowDevModeTutorial(value: Boolean)
|
||||
fun shouldShowStoresTutorial(): Boolean
|
||||
fun setShowStoresTutorial(value: Boolean)
|
||||
fun shouldShowHistoryTutorial(): Boolean
|
||||
fun setShowHistoryTutorial(value: Boolean)
|
||||
}
|
||||
|
||||
enum class TutorialPrefType { LAUNCH, DEV_MODE, STORES }
|
||||
enum class TutorialPrefType { LAUNCH, DEV_MODE, HISTORY }
|
||||
|
|
|
@ -14,6 +14,7 @@ import com.microsoft.device.samples.dualscreenexperience.data.order.model.OrderW
|
|||
|
||||
interface OrderDataSource {
|
||||
fun getOrderBySubmitted(submitted: Boolean): LiveData<OrderWithItems?>
|
||||
fun getAllSubmittedOrders(): LiveData<List<OrderWithItems>>
|
||||
|
||||
suspend fun getAll(): List<OrderWithItems>
|
||||
suspend fun getById(orderId: Long): OrderWithItems?
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package com.microsoft.device.samples.dualscreenexperience.data.order
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.microsoft.device.samples.dualscreenexperience.data.order.local.OrderLocalDataSource
|
||||
import com.microsoft.device.samples.dualscreenexperience.data.order.model.OrderEntity
|
||||
import com.microsoft.device.samples.dualscreenexperience.data.order.model.OrderItemEntity
|
||||
|
@ -22,6 +23,8 @@ class OrderRepository @Inject constructor(
|
|||
override fun getOrderBySubmitted(submitted: Boolean) =
|
||||
localDataSource.getOrderBySubmitted(submitted)
|
||||
|
||||
override fun getAllSubmittedOrders(): LiveData<List<OrderWithItems>> = localDataSource.getAllSubmittedOrders()
|
||||
|
||||
override suspend fun getAll(): List<OrderWithItems> = localDataSource.getAll()
|
||||
|
||||
override suspend fun getById(orderId: Long): OrderWithItems? = localDataSource.getById(orderId)
|
||||
|
|
|
@ -43,4 +43,8 @@ interface OrderDao {
|
|||
@Transaction
|
||||
@Query("SELECT * FROM orders where isSubmitted == :submitted LIMIT 1")
|
||||
fun getOrderBySubmitted(submitted: Boolean): LiveData<OrderWithItems?>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM orders where isSubmitted == 1")
|
||||
fun getAllSubmittedOrders(): LiveData<List<OrderWithItems>>
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package com.microsoft.device.samples.dualscreenexperience.data.order.local
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.microsoft.device.samples.dualscreenexperience.data.order.OrderDataSource
|
||||
import com.microsoft.device.samples.dualscreenexperience.data.order.model.OrderEntity
|
||||
import com.microsoft.device.samples.dualscreenexperience.data.order.model.OrderItemEntity
|
||||
|
@ -19,6 +20,8 @@ class OrderLocalDataSource @Inject constructor(
|
|||
|
||||
override fun getOrderBySubmitted(submitted: Boolean) = orderDao.getOrderBySubmitted(submitted)
|
||||
|
||||
override fun getAllSubmittedOrders(): LiveData<List<OrderWithItems>> = orderDao.getAllSubmittedOrders()
|
||||
|
||||
override suspend fun getAll(): List<OrderWithItems> = orderDao.getAll()
|
||||
|
||||
override suspend fun getById(orderId: Long) = orderDao.getById(orderId)
|
||||
|
|
|
@ -10,6 +10,7 @@ package com.microsoft.device.samples.dualscreenexperience.di
|
|||
import com.microsoft.device.samples.dualscreenexperience.presentation.MainNavigator
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.about.AboutNavigator
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.DevModeNavigator
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.history.HistoryNavigator
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.launch.LaunchNavigator
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderNavigator
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.product.ProductNavigator
|
||||
|
@ -41,6 +42,9 @@ object NavigationModule {
|
|||
@Provides
|
||||
fun provideOrderNavigator(navigator: MainNavigator): OrderNavigator = navigator
|
||||
|
||||
@Provides
|
||||
fun provideHistoryNavigator(navigator: MainNavigator): HistoryNavigator = navigator
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDevModeNavigator(): DevModeNavigator = DevModeNavigator()
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.domain.order.usecases
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.microsoft.device.samples.dualscreenexperience.data.order.OrderDataSource
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.model.Order
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetAllSubmittedOrdersUseCase @Inject constructor(private val orderRepository: OrderDataSource) {
|
||||
fun get(): LiveData<List<Order>> = Transformations.map(orderRepository.getAllSubmittedOrders()) { orderWithItems ->
|
||||
orderWithItems.map { Order(it) }
|
||||
}
|
||||
}
|
|
@ -42,6 +42,7 @@ import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.De
|
|||
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.DevModeViewModel.AppScreen
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.DevModeViewModel.DesignPattern
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.DevModeViewModel.SdkComponent
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.history.HistoryViewModel
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderViewModel
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.product.ProductViewModel
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.store.StoreViewModel
|
||||
|
@ -71,6 +72,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
private val productViewModel: ProductViewModel by viewModels()
|
||||
private val storeViewModel: StoreViewModel by viewModels()
|
||||
private val historyViewModel: HistoryViewModel by viewModels()
|
||||
|
||||
@VisibleForTesting
|
||||
private val orderViewModel: OrderViewModel by viewModels()
|
||||
|
@ -129,6 +131,7 @@ class MainActivity : AppCompatActivity() {
|
|||
R.id.fragment_store_map -> storeViewModel.reset()
|
||||
R.id.fragment_product_list -> productViewModel.reset()
|
||||
R.id.fragment_order -> orderViewModel.reset()
|
||||
R.id.fragment_history_list -> historyViewModel.reset()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,19 +150,20 @@ class MainActivity : AppCompatActivity() {
|
|||
when (item.itemId) {
|
||||
R.id.navigation_stores_graph -> {
|
||||
navigator.navigateToStores()
|
||||
hideStoresTutorial()
|
||||
}
|
||||
R.id.navigation_catalog_graph -> {
|
||||
navigator.navigateToCatalog()
|
||||
hideStoresTutorial()
|
||||
}
|
||||
R.id.navigation_products_graph -> {
|
||||
navigator.navigateToProducts()
|
||||
hideStoresTutorial()
|
||||
}
|
||||
R.id.navigation_orders_graph -> {
|
||||
navigator.navigateToOrders()
|
||||
}
|
||||
R.id.navigation_history_graph -> {
|
||||
navigator.navigateToHistory()
|
||||
hideHistoryTutorial()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
@ -194,23 +198,23 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun setupTutorialObserver() {
|
||||
tutorialViewModel.showStoresTutorial.observe(this) { isTutorialTriggered ->
|
||||
if (isTutorialTriggered == true && tutorialViewModel.shouldShowStoresTutorial()) {
|
||||
showStoresTutorial()
|
||||
tutorialViewModel.showHistoryTutorial.observe(this) { isTutorialTriggered ->
|
||||
if (isTutorialTriggered == true && tutorialViewModel.shouldShowHistoryTutorial()) {
|
||||
showHistoryTutorial()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showStoresTutorial() {
|
||||
val storeItem = findViewById<BottomNavigationItemView>(R.id.navigation_stores_graph)
|
||||
private fun showHistoryTutorial() {
|
||||
val historyItem = findViewById<BottomNavigationItemView>(R.id.navigation_history_graph)
|
||||
if (!isFinishing) {
|
||||
tutorial.show(storeItem, TutorialBalloonType.STORES)
|
||||
tutorial.show(historyItem, TutorialBalloonType.HISTORY)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideStoresTutorial() {
|
||||
tutorialViewModel.onStoresOpen()
|
||||
if (tutorial.currentBalloonType == TutorialBalloonType.STORES) {
|
||||
private fun hideHistoryTutorial() {
|
||||
tutorialViewModel.onHistoryOpen()
|
||||
if (tutorial.currentBalloonType == TutorialBalloonType.HISTORY) {
|
||||
tutorial.hide()
|
||||
}
|
||||
}
|
||||
|
@ -246,6 +250,8 @@ class MainActivity : AppCompatActivity() {
|
|||
setupDevMode(AppScreen.ORDER, DesignPattern.NONE, SdkComponent.RECYCLER_VIEW)
|
||||
R.id.fragment_order_receipt ->
|
||||
setupDevMode(AppScreen.ORDER, DesignPattern.NONE, SdkComponent.RECYCLER_VIEW)
|
||||
R.id.fragment_history_list ->
|
||||
setupDevMode(AppScreen.HISTORY_LIST_DETAILS, DesignPattern.LIST_DETAIL, SdkComponent.BOTTOM_NAVIGATION_VIEW)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -323,8 +329,10 @@ class MainActivity : AppCompatActivity() {
|
|||
@VisibleForTesting
|
||||
fun getSubmittedOrderLiveData() = orderViewModel.submittedOrder
|
||||
|
||||
fun getBottomNavViewHeight() = binding.bottomNavView.height
|
||||
|
||||
companion object {
|
||||
const val HIDE_BOTTOM_BAR_KEY = "hideBottomNav"
|
||||
const val BOTTOM_NAV_ITEM_COUNT = 4
|
||||
const val BOTTOM_NAV_ITEM_COUNT = 5
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,11 +12,12 @@ import androidx.navigation.FoldableNavOptions
|
|||
import com.microsoft.device.dualscreen.navigation.LaunchScreen
|
||||
import com.microsoft.device.samples.dualscreenexperience.R
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.catalog.CatalogNavigator
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.history.HistoryNavigator
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderNavigator
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.product.ProductNavigator
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.store.StoreNavigator
|
||||
|
||||
class MainNavigator : StoreNavigator, CatalogNavigator, ProductNavigator, OrderNavigator {
|
||||
class MainNavigator : StoreNavigator, CatalogNavigator, ProductNavigator, OrderNavigator, HistoryNavigator {
|
||||
private var navController: FoldableNavController? = null
|
||||
|
||||
fun bind(navController: FoldableNavController) {
|
||||
|
@ -81,4 +82,13 @@ class MainNavigator : StoreNavigator, CatalogNavigator, ProductNavigator, OrderN
|
|||
override fun navigateToOrderReceipt() {
|
||||
navController?.navigate(R.id.action_order_to_receipt)
|
||||
}
|
||||
|
||||
override fun navigateToHistory() {
|
||||
val navOptions = FoldableNavOptions.Builder().setLaunchScreen(LaunchScreen.BOTH).build()
|
||||
navController?.navigate(R.id.navigation_history_graph, null, navOptions)
|
||||
}
|
||||
|
||||
override fun navigateToHistoryDetails() {
|
||||
navController?.navigate(R.id.action_history_list_to_details)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,8 +28,8 @@ import com.microsoft.device.dualscreen.windowstate.FoldState
|
|||
import com.microsoft.device.dualscreen.windowstate.rememberWindowState
|
||||
import com.microsoft.device.samples.dualscreenexperience.R
|
||||
import com.microsoft.device.samples.dualscreenexperience.databinding.FragmentCatalogBinding
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui.theme.CatalogTheme
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui.view.Catalog
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui.Catalog
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.theme.DualScreenExperienceTheme
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.appCompatActivity
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.changeToolbarTitle
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.hasExpandedWindowLayoutSize
|
||||
|
@ -75,7 +75,7 @@ class CatalogListFragment : Fragment() {
|
|||
// is destroyed
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
CatalogTheme {
|
||||
DualScreenExperienceTheme {
|
||||
val windowState = appCompatActivity?.rememberWindowState()
|
||||
if (windowState != null) {
|
||||
val isFeatureFoldHorizontal =
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui.view
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
@ -1,12 +1,11 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui.view
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
|
@ -1,11 +1,11 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui.view
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
@ -1,11 +1,11 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui.view
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
|
@ -1,11 +1,11 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui.view
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
|
@ -1,11 +1,11 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui.view
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
|
@ -1,11 +1,11 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui.view
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui
|
||||
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
|
@ -1,11 +1,11 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui.view
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple200 = Color(0xFFBB86FC)
|
||||
val Purple500 = Color(0xFF6200EE)
|
||||
val Purple700 = Color(0xFF3700B3)
|
||||
val Teal200 = Color(0xFF03DAC5)
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui.theme
|
||||
|
||||
import androidx.compose.material.Typography
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.microsoft.device.samples.dualscreenexperience.R
|
||||
|
||||
val Typography = Typography(
|
||||
h5 = TextStyle(
|
||||
fontFamily = FontFamily.Serif,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 24.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
h6 = TextStyle(
|
||||
fontFamily = FontFamily(Font(R.font.roboto)),
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 18.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
body1 = TextStyle(
|
||||
fontFamily = FontFamily(Font(R.font.dmsans_regular)),
|
||||
fontWeight = FontWeight.Normal,
|
||||
lineHeight = 24.sp,
|
||||
fontSize = 16.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
body2 = TextStyle(
|
||||
fontFamily = FontFamily.Serif,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontStyle = FontStyle.Italic,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp
|
||||
),
|
||||
caption = TextStyle(
|
||||
fontFamily = FontFamily.Serif,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
)
|
|
@ -82,7 +82,8 @@ class DevModeViewModel @Inject constructor(
|
|||
CATALOG("catalog/CatalogListFragment.kt"),
|
||||
PRODUCTS_LIST_DETAILS("product/details/ProductDetailsFragment.kt"),
|
||||
PRODUCTS_CUSTOMIZE("product/customize/ProductCustomizeFragment.kt"),
|
||||
ORDER("order/OrderFragment.kt");
|
||||
ORDER("order/OrderFragment.kt"),
|
||||
HISTORY_LIST_DETAILS("history/HistoryListFragment.kt");
|
||||
|
||||
fun buildUrl() = "$APP_BASE_URL/$path"
|
||||
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.history
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.microsoft.device.dualscreen.windowstate.rememberWindowState
|
||||
import com.microsoft.device.samples.dualscreenexperience.R
|
||||
import com.microsoft.device.samples.dualscreenexperience.databinding.FragmentHistoryDetailBinding
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.MainActivity
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.history.ui.OrderHistoryDetailPage
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.product.ProductViewModel
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.theme.DualScreenExperienceTheme
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.LayoutInfoViewModel
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.appCompatActivity
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.changeToolbarTitle
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.isExpanded
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.isLandscape
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.isSmallHeight
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.isSmallWidth
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.setupToolbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HistoryDetailFragment : Fragment() {
|
||||
|
||||
private val viewModel: HistoryViewModel by activityViewModels()
|
||||
private val layoutInfoViewModel: LayoutInfoViewModel by activityViewModels()
|
||||
private val productViewModel: ProductViewModel by activityViewModels()
|
||||
|
||||
private var binding: FragmentHistoryDetailBinding? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = FragmentHistoryDetailBinding.inflate(inflater, container, false)
|
||||
binding?.lifecycleOwner = this
|
||||
binding?.composeView?.apply {
|
||||
// Dispose of the Composition when the view's LifecycleOwner is destroyed
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
val windowState = activity?.rememberWindowState()
|
||||
|
||||
DualScreenExperienceTheme {
|
||||
OrderHistoryDetailPage(
|
||||
order = viewModel.selectedOrder.observeAsState().value,
|
||||
isDualMode = layoutInfoViewModel.isDualMode.observeAsState().value,
|
||||
topBarPadding = appCompatActivity?.supportActionBar?.height ?: 0,
|
||||
bottomNavPadding = (activity as? MainActivity)?.getBottomNavViewHeight() ?: 0,
|
||||
isLandscape = windowState?.isLandscape() ?: false,
|
||||
isExpanded = windowState?.isExpanded() ?: false,
|
||||
isSmallWidth = windowState?.isSmallWidth() ?: false,
|
||||
getProductFromOrderItem = productViewModel::getProductFromOrderItem,
|
||||
addToOrder = viewModel::addItemToOrder,
|
||||
isSmallHeight = windowState?.isSmallHeight() ?: false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return binding?.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
setupToolbar()
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
if (layoutInfoViewModel.isDualMode.value == false) {
|
||||
appCompatActivity?.changeToolbarTitle(getString(R.string.toolbar_history_details_title))
|
||||
appCompatActivity?.setupToolbar(isBackButtonEnabled = true, viewLifecycleOwner) {
|
||||
viewModel.navigateUp()
|
||||
viewModel.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
binding = null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.history
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.window.layout.WindowInfoTracker
|
||||
import androidx.window.layout.WindowLayoutInfo
|
||||
import com.microsoft.device.dualscreen.utils.wm.isInDualMode
|
||||
import com.microsoft.device.dualscreen.windowstate.rememberWindowState
|
||||
import com.microsoft.device.samples.dualscreenexperience.R
|
||||
import com.microsoft.device.samples.dualscreenexperience.databinding.FragmentHistoryListBinding
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.MainActivity
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.history.ui.OrderHistoryListPage
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.theme.DualScreenExperienceTheme
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.LayoutInfoViewModel
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.appCompatActivity
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.changeToolbarTitle
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.isLandscape
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.isSmallWidth
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.setupToolbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HistoryListFragment : Fragment() {
|
||||
|
||||
private val viewModel: HistoryViewModel by activityViewModels()
|
||||
private val layoutInfoViewModel: LayoutInfoViewModel by activityViewModels()
|
||||
private var binding: FragmentHistoryListBinding? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
observeWindowLayoutInfo(context as AppCompatActivity)
|
||||
}
|
||||
|
||||
private fun observeWindowLayoutInfo(activity: AppCompatActivity) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
WindowInfoTracker.getOrCreate(activity)
|
||||
.windowLayoutInfo(activity)
|
||||
.collect {
|
||||
onWindowLayoutInfoChanged(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = FragmentHistoryListBinding.inflate(inflater, container, false)
|
||||
binding?.lifecycleOwner = this
|
||||
binding?.composeView?.apply {
|
||||
// Dispose of the Composition when the view's LifecycleOwner is destroyed
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
val windowState = activity?.rememberWindowState()
|
||||
|
||||
DualScreenExperienceTheme {
|
||||
OrderHistoryListPage(
|
||||
orders = viewModel.orderList.observeAsState().value?.reversed(),
|
||||
selectedOrder = viewModel.selectedOrder.observeAsState().value,
|
||||
updateOrder = { newOrder ->
|
||||
viewModel.navigateToDetails()
|
||||
viewModel.selectOrder(newOrder)
|
||||
},
|
||||
topBarPadding = appCompatActivity?.supportActionBar?.height ?: 0,
|
||||
bottomNavPadding = (activity as? MainActivity)?.getBottomNavViewHeight() ?: 0,
|
||||
isLandscape = windowState?.isLandscape() ?: false,
|
||||
isSmallWidth = windowState?.isSmallWidth() ?: false,
|
||||
isDualMode = layoutInfoViewModel.isDualMode.observeAsState().value,
|
||||
windowState = windowState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return binding?.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
setupToolbar()
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
appCompatActivity?.changeToolbarTitle(getString(R.string.toolbar_history_title))
|
||||
appCompatActivity?.setupToolbar(isBackButtonEnabled = false) {}
|
||||
}
|
||||
|
||||
private fun onWindowLayoutInfoChanged(windowLayoutInfo: WindowLayoutInfo) {
|
||||
if (windowLayoutInfo.isInDualMode() && viewModel.orderList.value?.isNotEmpty() == true &&
|
||||
viewModel.selectedOrder.value == null
|
||||
) {
|
||||
viewModel.selectMostRecentOrder()
|
||||
viewModel.navigateToDetails()
|
||||
} else if (!windowLayoutInfo.isInDualMode() && viewModel.selectedOrder.value != null) {
|
||||
viewModel.navigateToDetails()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
binding = null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.history
|
||||
|
||||
interface HistoryNavigator {
|
||||
fun navigateToHistory()
|
||||
fun navigateToHistoryDetails()
|
||||
fun navigateUp()
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.history
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.model.Order
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.model.OrderItem
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.usecases.AddItemToOrderUseCase
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.usecases.GetAllSubmittedOrdersUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HistoryViewModel @Inject constructor(
|
||||
getAllOrdersUseCase: GetAllSubmittedOrdersUseCase,
|
||||
private val addItemUseCase: AddItemToOrderUseCase,
|
||||
private val navigator: HistoryNavigator
|
||||
) : ViewModel() {
|
||||
var orderList: LiveData<List<Order>> = getAllOrdersUseCase.get()
|
||||
var selectedOrder = MutableLiveData<Order?>(null)
|
||||
|
||||
fun reset() {
|
||||
selectedOrder.value = null
|
||||
}
|
||||
|
||||
fun onClick(model: Order) {
|
||||
navigateToDetails()
|
||||
selectOrder(model)
|
||||
}
|
||||
|
||||
fun navigateUp() {
|
||||
navigator.navigateUp()
|
||||
}
|
||||
|
||||
fun navigateToDetails() {
|
||||
navigator.navigateToHistoryDetails()
|
||||
}
|
||||
|
||||
fun selectMostRecentOrder() {
|
||||
orderList.value?.takeIf { it.isNotEmpty() && selectedOrder.value == null }?.let { list ->
|
||||
selectOrder(list[list.size - 1])
|
||||
}
|
||||
}
|
||||
|
||||
fun selectOrder(order: Order) {
|
||||
selectedOrder.value = order
|
||||
}
|
||||
|
||||
fun addItemToOrder(item: OrderItem) {
|
||||
viewModelScope.launch {
|
||||
// Add copy of previous order item to new order
|
||||
addItemUseCase.addToOrder(item.copy())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,266 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.history.ui
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.drawscope.withTransform
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.airbnb.lottie.compose.LottieAnimation
|
||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||
import com.airbnb.lottie.compose.animateLottieCompositionAsState
|
||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||
import com.microsoft.device.samples.dualscreenexperience.R
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.model.OrderItem
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.product.model.Product
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.catalog.utils.contentDescription
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.product.util.StarRatingView
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.product.util.getProductDrawable
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.toPriceString
|
||||
|
||||
@Composable
|
||||
fun AddToOrderDialog(
|
||||
selectedOrderItem: OrderItem?,
|
||||
updateShowDialog: (Boolean) -> Unit,
|
||||
getProductFromOrderItem: (OrderItem) -> Product?,
|
||||
addToOrder: (OrderItem) -> Unit,
|
||||
isSmallHeight: Boolean
|
||||
) {
|
||||
if (selectedOrderItem == null) {
|
||||
updateShowDialog(false)
|
||||
return
|
||||
}
|
||||
|
||||
val bottomRoundShape = MaterialTheme.shapes.medium.copy(
|
||||
topEnd = CornerSize(0.dp), topStart = CornerSize(0.dp)
|
||||
)
|
||||
|
||||
Dialog(onDismissRequest = { updateShowDialog(false) }) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.secondary, MaterialTheme.shapes.medium),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
DialogOrderPreview(orderItem = selectedOrderItem)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 303.dp)
|
||||
.background(MaterialTheme.colors.surface, bottomRoundShape)
|
||||
.padding(horizontal = 32.dp, vertical = if (isSmallHeight) 15.dp else 32.dp),
|
||||
verticalArrangement = spacedBy(8.dp)
|
||||
) {
|
||||
DialogOrderTitle(orderItem = selectedOrderItem, isSmallHeight = isSmallHeight)
|
||||
DialogOrderRating(orderItem = selectedOrderItem, getProductFromOrderItem = getProductFromOrderItem)
|
||||
DialogOrderPrice(orderItem = selectedOrderItem)
|
||||
DialogOrderDescription(
|
||||
orderItem = selectedOrderItem,
|
||||
getProductFromOrderItem = getProductFromOrderItem,
|
||||
isSmallHeight = isSmallHeight
|
||||
)
|
||||
Spacer(Modifier.heightIn(min = 0.dp, max = 14.dp))
|
||||
DialogButtons(orderItem = selectedOrderItem, addToOrder = addToOrder, updateShowDialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DialogOrderPreview(orderItem: OrderItem) {
|
||||
val resId = getProductDrawable(orderItem.color, orderItem.bodyShape)
|
||||
val vector = ImageVector.vectorResource(id = resId)
|
||||
val painter = rememberVectorPainter(image = vector)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.padding(vertical = 30.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Canvas(
|
||||
modifier = Modifier.size(height = 65.dp, width = 239.dp)
|
||||
) {
|
||||
// Scale drawable to fit in canvas - since the drawable will be rotated 90 deg, the
|
||||
// desired height of the drawable will actually be the canvas width
|
||||
val desiredHeight = size.width
|
||||
val desiredWidth = desiredHeight * painter.intrinsicSize.width / painter.intrinsicSize.height
|
||||
|
||||
// Translate drawable to center of canvas before rotation
|
||||
val translateX = (size.width - desiredWidth) / 2
|
||||
val translateY = (desiredHeight - size.height) / 2
|
||||
|
||||
withTransform(
|
||||
{
|
||||
rotate(90F)
|
||||
translate(left = translateX, top = -translateY)
|
||||
}
|
||||
) {
|
||||
with(painter) {
|
||||
draw(Size(width = desiredWidth, height = desiredHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DialogOrderTitle(orderItem: OrderItem, isSmallHeight: Boolean) {
|
||||
val textStyle = if (isSmallHeight) MaterialTheme.typography.h2.copy(fontSize = 20.sp) else MaterialTheme.typography.h2
|
||||
|
||||
Text(
|
||||
text = orderItem.name,
|
||||
style = textStyle,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DialogOrderRating(orderItem: OrderItem, getProductFromOrderItem: (OrderItem) -> Product?) {
|
||||
AndroidView(
|
||||
factory = {
|
||||
StarRatingView(it).apply {
|
||||
val product = getProductFromOrderItem(orderItem)
|
||||
product?.let { setValue(product.rating) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DialogOrderPrice(orderItem: OrderItem) {
|
||||
Text(
|
||||
text = orderItem.price.toFloat().toPriceString(),
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DialogOrderDescription(
|
||||
orderItem: OrderItem,
|
||||
getProductFromOrderItem: (OrderItem) -> Product?,
|
||||
isSmallHeight: Boolean
|
||||
) {
|
||||
// Limit description to one line of scrollable text when dialog is shown with a small height
|
||||
val textStyle = MaterialTheme.typography.subtitle1
|
||||
val lineHeightDp = with(LocalDensity.current) { textStyle.lineHeight.toDp() }
|
||||
val maxHeight = if (isSmallHeight) (2 * lineHeightDp.value).dp else Dp.Unspecified
|
||||
val bottomPadding = if (isSmallHeight) lineHeightDp else 0.dp
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.heightIn(max = maxHeight)
|
||||
.padding(bottom = bottomPadding)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
text = getProductFromOrderItem(orderItem)?.description ?: "",
|
||||
style = textStyle,
|
||||
overflow = TextOverflow.Visible,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.DialogButtons(
|
||||
orderItem: OrderItem,
|
||||
addToOrder: (OrderItem) -> Unit,
|
||||
updateShowDialog: (Boolean) -> Unit
|
||||
) {
|
||||
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.add_to_order_animation))
|
||||
var isPlaying by remember { mutableStateOf(false) }
|
||||
val progress by animateLottieCompositionAsState(composition, isPlaying = isPlaying)
|
||||
|
||||
val buttonShape = RoundedCornerShape(100.dp)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.height(IntrinsicSize.Max),
|
||||
horizontalArrangement = spacedBy(8.dp)
|
||||
) {
|
||||
TextButton(
|
||||
onClick = { updateShowDialog(false) },
|
||||
shape = buttonShape,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colors.primary),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary.copy(alpha = .08f),
|
||||
contentColor = MaterialTheme.colors.onBackground
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.order_sign_cancel),
|
||||
style = MaterialTheme.typography.caption.copy(fontSize = 14.sp)
|
||||
)
|
||||
}
|
||||
LottieAnimation(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(buttonShape)
|
||||
.pointerInput(orderItem) {
|
||||
detectTapGestures {
|
||||
// Start animating if not already in progress
|
||||
if (progress == 0f) {
|
||||
isPlaying = true
|
||||
addToOrder(orderItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
.contentDescription(stringResource(R.string.product_accessibility_add_to_order)),
|
||||
composition = composition,
|
||||
progress = { progress },
|
||||
maintainOriginalImageBounds = true,
|
||||
contentScale = ContentScale.FillWidth
|
||||
)
|
||||
}
|
||||
|
||||
// Close dialog when animation is complete
|
||||
if (isPlaying && progress == 1f) {
|
||||
updateShowDialog(false)
|
||||
isPlaying = false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.history.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
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.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.microsoft.device.dualscreen.windowstate.WindowState
|
||||
import com.microsoft.device.samples.dualscreenexperience.R
|
||||
|
||||
@Composable
|
||||
fun PlaceholderOrderHistory(isDualMode: Boolean?, windowState: WindowState?, topBarPaddingDp: Dp, bottomNavPaddingDp: Dp) {
|
||||
if (isDualMode == true) {
|
||||
PlaceholderTwoPane(windowState, topBarPaddingDp, bottomNavPaddingDp)
|
||||
} else {
|
||||
PlaceholderSinglePane()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlaceholderSinglePane() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
PlaceholderImage()
|
||||
Spacer(modifier = Modifier.fillMaxHeight(0.1f))
|
||||
PlaceholderText()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlaceholderTwoPane(windowState: WindowState?, topBarPaddingDp: Dp, bottomNavPaddingDp: Dp) {
|
||||
if (windowState?.isDualPortrait() == true) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.size(windowState.pane1SizeDp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
PlaceholderText()
|
||||
}
|
||||
Spacer(Modifier.width(windowState.foldSizeDp))
|
||||
Box(
|
||||
modifier = Modifier.size(windowState.pane2SizeDp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
PlaceholderImage()
|
||||
}
|
||||
}
|
||||
} else if (windowState?.isDualLandscape() == true) {
|
||||
val pane1Size = windowState.pane1SizeDp.copy(height = windowState.pane1SizeDp.height - topBarPaddingDp)
|
||||
val pane2Size = windowState.pane2SizeDp.copy(height = windowState.pane2SizeDp.height - bottomNavPaddingDp)
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier.size(pane1Size),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
PlaceholderImage()
|
||||
}
|
||||
Spacer(Modifier.height(windowState.foldSizeDp))
|
||||
Box(
|
||||
modifier = Modifier.size(pane2Size),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
PlaceholderText(centerText = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlaceholderImage() {
|
||||
Image(
|
||||
modifier = Modifier.fillMaxWidth(0.5f),
|
||||
painter = painterResource(R.drawable.empty_boxes),
|
||||
contentDescription = null, // null content description indicates that image is decorative
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlaceholderText(centerText: Boolean = false) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(0.75f),
|
||||
text = stringResource(R.string.order_history_empty_message),
|
||||
style = MaterialTheme.typography.h3,
|
||||
textAlign = if (centerText) TextAlign.Center else TextAlign.Start,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
|
@ -0,0 +1,293 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.history.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.sizeIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.microsoft.device.samples.dualscreenexperience.R
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.model.Order
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.model.OrderItem
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.product.model.Product
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.product.util.getProductContentDescription
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.product.util.getProductDrawable
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.toPriceString
|
||||
|
||||
@Composable
|
||||
fun OrderHistoryDetailPage(
|
||||
order: Order?,
|
||||
isDualMode: Boolean?,
|
||||
topBarPadding: Int,
|
||||
bottomNavPadding: Int,
|
||||
isLandscape: Boolean,
|
||||
isExpanded: Boolean,
|
||||
isSmallWidth: Boolean,
|
||||
getProductFromOrderItem: (OrderItem) -> Product?,
|
||||
addToOrder: (OrderItem) -> Unit,
|
||||
isSmallHeight: Boolean
|
||||
) {
|
||||
if (order == null || isDualMode == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate padding for LazyColumn
|
||||
val paddingValues = with(LocalDensity.current) {
|
||||
PaddingValues(bottom = 20.dp + topBarPadding.toDp() + bottomNavPadding.toDp())
|
||||
}
|
||||
|
||||
val columnModifier = if (isLandscape)
|
||||
Modifier
|
||||
.fillMaxWidth(0.915f)
|
||||
.padding(top = 32.dp)
|
||||
.fillMaxHeight()
|
||||
else
|
||||
Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.fillMaxHeight()
|
||||
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
val updateShowDialog = { newValue: Boolean -> showDialog = newValue }
|
||||
var selectedOrderItem: OrderItem? by remember { mutableStateOf(null) }
|
||||
val updateOrderItem = { newItem: OrderItem -> selectedOrderItem = newItem }
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
||||
Column(
|
||||
modifier = columnModifier,
|
||||
) {
|
||||
OrderHeader(order, isDualMode, isSmallWidth)
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
OrderItems(order.items, paddingValues, isExpanded, isSmallWidth, updateShowDialog, updateOrderItem)
|
||||
}
|
||||
if (showDialog)
|
||||
AddToOrderDialog(
|
||||
selectedOrderItem,
|
||||
updateShowDialog,
|
||||
getProductFromOrderItem,
|
||||
addToOrder,
|
||||
isSmallHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OrderHeader(order: Order, isDualMode: Boolean, isSmallWidth: Boolean) {
|
||||
if (isDualMode) {
|
||||
Text(
|
||||
text = stringResource(R.string.toolbar_history_details_title),
|
||||
style = MaterialTheme.typography.h4,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
OrderDate(
|
||||
modifier = Modifier.weight(1f),
|
||||
orderTimestamp = order.orderTimestamp,
|
||||
isSmallWidth = isSmallWidth
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
OrderId(orderId = order.orderId, isSmallWidth = isSmallWidth)
|
||||
OrderAmount(orderPrice = order.totalPrice, isSmallWidth = isSmallWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OrderItems(
|
||||
orderItems: MutableList<OrderItem>,
|
||||
paddingValues: PaddingValues,
|
||||
isExpanded: Boolean,
|
||||
isSmallWidth: Boolean,
|
||||
updateShowDialog: (Boolean) -> Unit,
|
||||
updateOrderItem: (OrderItem) -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
verticalArrangement = spacedBy(25.dp),
|
||||
contentPadding = paddingValues
|
||||
) {
|
||||
orderItems.map { orderItem ->
|
||||
item {
|
||||
OrderItem(orderItem, isExpanded, isSmallWidth, updateShowDialog, updateOrderItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OrderItem(
|
||||
orderItem: OrderItem,
|
||||
isExpanded: Boolean,
|
||||
isSmallWidth: Boolean,
|
||||
updateShowDialog: (Boolean) -> Unit,
|
||||
updateOrderItem: (OrderItem) -> Unit
|
||||
) {
|
||||
val initialSizePx = with(LocalDensity.current) { 100.dp.toPx() }
|
||||
var boxWidthPx by remember { mutableStateOf(initialSizePx.toInt()) }
|
||||
val updateBoxWidthPx = { newWidth: Int -> boxWidthPx = newWidth }
|
||||
|
||||
Box {
|
||||
OrderItemDetails(orderItem, isExpanded, isSmallWidth, updateBoxWidthPx, updateShowDialog, updateOrderItem)
|
||||
OrderItemImage(orderItem = orderItem, boxWidthPx = boxWidthPx)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoxScope.OrderItemDetails(
|
||||
orderItem: OrderItem,
|
||||
isExpanded: Boolean,
|
||||
isSmallWidth: Boolean,
|
||||
updateBoxWidthPx: (Int) -> Unit,
|
||||
updateShowDialog: (Boolean) -> Unit,
|
||||
updateOrderItem: (OrderItem) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Max)
|
||||
.align(Alignment.BottomCenter),
|
||||
horizontalArrangement = spacedBy(41.dp)
|
||||
) {
|
||||
OrderItemImageBackground(isExpanded, updateBoxWidthPx)
|
||||
OrderItemText(orderItem, isExpanded, isSmallWidth)
|
||||
ViewButton(
|
||||
onClick = {
|
||||
updateShowDialog(true)
|
||||
updateOrderItem(orderItem)
|
||||
},
|
||||
isSmallWidth = isSmallWidth
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RowScope.OrderItemText(orderItem: OrderItem, isExpanded: Boolean, isSmallWidth: Boolean) {
|
||||
val rowWeight = if (isExpanded) 7f else 5f
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(rowWeight)
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = spacedBy(6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = orderItem.name,
|
||||
style = if (isSmallWidth) MaterialTheme.typography.subtitle2 else MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Text(
|
||||
text = orderItem.price.toFloat().toPriceString(),
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.order_quantity, orderItem.quantity),
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RowScope.OrderItemImageBackground(isExpanded: Boolean, updateBoxWidthPx: (Int) -> Unit) {
|
||||
val rowWeight = if (isExpanded) 2f else 3f
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(rowWeight)
|
||||
.align(Alignment.Bottom),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.sizeIn(minWidth = 0.dp, minHeight = 0.dp, maxWidth = 100.dp, maxHeight = 100.dp)
|
||||
.aspectRatio(1f)
|
||||
.onSizeChanged { updateBoxWidthPx(it.width) }
|
||||
.align(Alignment.BottomStart),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) { }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OrderItemImage(orderItem: OrderItem, boxWidthPx: Int) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.heightIn(max = 224.dp)
|
||||
.graphicsLayer(
|
||||
rotationZ = 30f,
|
||||
translationX = boxWidthPx / 2f
|
||||
),
|
||||
painter = painterResource(getProductDrawable(orderItem.color, orderItem.bodyShape)),
|
||||
contentDescription = stringResource(getProductContentDescription(orderItem.color, orderItem.bodyShape))
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RowScope.ViewButton(onClick: () -> Unit, isSmallWidth: Boolean) {
|
||||
val baseTextStyle = MaterialTheme.typography.caption
|
||||
|
||||
TextButton(
|
||||
modifier = Modifier
|
||||
.weight(3f)
|
||||
.align(Alignment.Bottom),
|
||||
shape = CircleShape,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary,
|
||||
contentColor = MaterialTheme.colors.onPrimary
|
||||
),
|
||||
onClick = { onClick() }
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.order_history_view),
|
||||
style = if (isSmallWidth) baseTextStyle.copy(fontSize = 14.sp) else baseTextStyle,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.history.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
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.fillMaxHeight
|
||||
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.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.microsoft.device.dualscreen.windowstate.WindowState
|
||||
import com.microsoft.device.samples.dualscreenexperience.R
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.model.Order
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.model.OrderItem
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.product.util.getProductContentDescription
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.product.util.getProductDrawable
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.toDateString
|
||||
|
||||
const val NUM_IMAGES_MAX = 3
|
||||
|
||||
@Composable
|
||||
fun OrderHistoryListPage(
|
||||
orders: List<Order>?,
|
||||
selectedOrder: Order?,
|
||||
updateOrder: (Order) -> Unit,
|
||||
topBarPadding: Int,
|
||||
bottomNavPadding: Int,
|
||||
isLandscape: Boolean,
|
||||
isSmallWidth: Boolean,
|
||||
isDualMode: Boolean?,
|
||||
windowState: WindowState?
|
||||
) {
|
||||
// Calculate padding for LazyColumn
|
||||
val topBarPaddingDp = with(LocalDensity.current) { topBarPadding.toDp() }
|
||||
val bottomNavPaddingDp = with(LocalDensity.current) { bottomNavPadding.toDp() }
|
||||
val paddingValues = PaddingValues(bottom = 20.dp + topBarPaddingDp + bottomNavPaddingDp)
|
||||
|
||||
if (orders.isNullOrEmpty())
|
||||
PlaceholderOrderHistory(isDualMode, windowState, topBarPaddingDp, bottomNavPaddingDp)
|
||||
else
|
||||
OrderList(orders, selectedOrder, updateOrder, paddingValues, isLandscape, isSmallWidth)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OrderList(
|
||||
orders: List<Order>?,
|
||||
selectedOrder: Order?,
|
||||
updateOrder: (Order) -> Unit,
|
||||
paddingValues: PaddingValues,
|
||||
isLandscape: Boolean,
|
||||
isSmallWidth: Boolean
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = spacedBy(12.dp),
|
||||
contentPadding = paddingValues
|
||||
) {
|
||||
orders?.map { order ->
|
||||
item {
|
||||
OrderCard(order, order == selectedOrder, updateOrder, isLandscape, isSmallWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OrderCard(
|
||||
order: Order,
|
||||
isSelected: Boolean,
|
||||
updateOrder: (Order) -> Unit,
|
||||
isLandscape: Boolean,
|
||||
isSmallWidth: Boolean
|
||||
) {
|
||||
// Store height of order item text so the order item box is always large enough to contain the text
|
||||
var orderItemTextHeight by remember { mutableStateOf(123.dp) }
|
||||
val updateTextHeight = { newHeight: Dp ->
|
||||
val newHeightWithPadding = newHeight + 32.dp
|
||||
if (newHeightWithPadding > orderItemTextHeight)
|
||||
orderItemTextHeight = newHeightWithPadding
|
||||
}
|
||||
|
||||
Box(contentAlignment = Alignment.BottomCenter) {
|
||||
CardBackground(isSelected, { updateOrder(order) }, orderItemTextHeight)
|
||||
OrderGraphicAndText(order, isLandscape, isSmallWidth, updateTextHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OrderGraphicAndText(
|
||||
order: Order,
|
||||
isLandscape: Boolean,
|
||||
isSmallWidth: Boolean,
|
||||
updateTextHeight: (Dp) -> Unit
|
||||
) {
|
||||
val previewWeight = 1f
|
||||
val textWeight = if (isSmallWidth) 1.5f else 2f
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(if (isLandscape) 0.884f else 0.897f)
|
||||
.padding(bottom = 16.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
horizontalArrangement = if (isSmallWidth || !isLandscape) spacedBy(40.dp) else spacedBy(88.dp)
|
||||
) {
|
||||
OrderGraphic(Modifier.weight(previewWeight), order.items, isSmallWidth)
|
||||
OrderText(Modifier.weight(textWeight), order, isSmallWidth, updateTextHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OrderText(
|
||||
modifier: Modifier = Modifier,
|
||||
order: Order,
|
||||
isSmallWidth: Boolean,
|
||||
updateTextHeight: (Dp) -> Unit
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
|
||||
Column(
|
||||
modifier = modifier.onSizeChanged {
|
||||
val heightDp = with(density) { it.height.toDp() }
|
||||
updateTextHeight(heightDp)
|
||||
},
|
||||
verticalArrangement = spacedBy(12.dp)
|
||||
) {
|
||||
OrderDate(orderTimestamp = order.orderTimestamp, isSmallWidth = isSmallWidth)
|
||||
OrderId(orderId = order.orderId, isSmallWidth = isSmallWidth)
|
||||
OrderAmount(orderPrice = order.totalPrice, isSmallWidth = isSmallWidth)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OrderDate(modifier: Modifier = Modifier, orderTimestamp: Long, isSmallWidth: Boolean) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = stringResource(R.string.order_date, orderTimestamp.toDateString()),
|
||||
style = if (isSmallWidth) MaterialTheme.typography.subtitle2 else MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OrderId(modifier: Modifier = Modifier, orderId: Long?, isSmallWidth: Boolean) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = stringResource(R.string.order_id, orderId ?: ""),
|
||||
style = if (isSmallWidth) MaterialTheme.typography.subtitle2 else MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OrderAmount(modifier: Modifier = Modifier, orderPrice: Int, isSmallWidth: Boolean) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = stringResource(R.string.order_amount, orderPrice),
|
||||
style = if (isSmallWidth) MaterialTheme.typography.subtitle2 else MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CardBackground(isSelected: Boolean, updateOrder: () -> Unit, minHeight: Dp) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = minHeight)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.clickable { updateOrder() },
|
||||
color = if (isSelected) MaterialTheme.colors.secondary else MaterialTheme.colors.surface
|
||||
) {}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OrderGraphic(modifier: Modifier = Modifier, orderItems: MutableList<OrderItem>, isSmallWidth: Boolean) {
|
||||
val endIndex = if (orderItems.count() >= NUM_IMAGES_MAX) NUM_IMAGES_MAX else orderItems.count()
|
||||
val items = orderItems.subList(0, endIndex)
|
||||
val overlap = if (isSmallWidth) 30.dp else 37.dp
|
||||
|
||||
// Calculate guitar image offsets so the order item preview is always centered
|
||||
val offsets = when (items.size) {
|
||||
2 -> listOf(-overlap / 2, overlap / 2)
|
||||
3 -> listOf(-overlap, 0.dp, overlap)
|
||||
else -> listOf(0.dp, 0.dp, 0.dp)
|
||||
}
|
||||
|
||||
Box(modifier = modifier, contentAlignment = Alignment.BottomCenter) {
|
||||
items.mapIndexed { index, orderItem ->
|
||||
OrderItemImage(offsets[index], orderItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OrderItemImage(xOffset: Dp, orderItem: OrderItem) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.widthIn(min = 50.dp)
|
||||
.heightIn(max = 155.dp)
|
||||
.offset(x = xOffset),
|
||||
painter = painterResource(id = getProductDrawable(orderItem.color, orderItem.bodyShape)),
|
||||
contentDescription = stringResource(id = getProductContentDescription(orderItem.color, orderItem.bodyShape))
|
||||
)
|
||||
}
|
|
@ -10,6 +10,7 @@ package com.microsoft.device.samples.dualscreenexperience.presentation.product
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.model.OrderItem
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.product.model.Product
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.product.usecases.GetProductsUseCase
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.util.DataListHandler
|
||||
|
@ -63,4 +64,10 @@ class ProductViewModel @Inject constructor(
|
|||
private fun selectProduct(product: Product?) {
|
||||
selectedProduct.value = product
|
||||
}
|
||||
|
||||
fun getProductFromOrderItem(orderItem: OrderItem): Product? {
|
||||
return productList.value?.firstOrNull {
|
||||
it.name == orderItem.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val BackgroundLight = Color(0xFFFAFAFA)
|
||||
val BackgroundGray = Color(0xFF1F2228)
|
||||
|
||||
val Orange = Color(0xFFF6B63D)
|
||||
|
||||
val PrimaryGold = Color(0xFFDDCAB3)
|
||||
val PrimaryDarkGold = Color(0xFF4A2916)
|
||||
|
||||
val SurfaceDarkBlue = Color(0xFF2A3037)
|
||||
val SurfaceLight = Color(0xFFF2EEEB)
|
||||
|
||||
val FocusLightOrange = Color(0xFFF2E2D5)
|
||||
val FocusBlueGray = Color(0xFF374452)
|
|
@ -5,7 +5,7 @@
|
|||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui.theme
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.theme
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Shapes
|
||||
|
@ -13,6 +13,6 @@ import androidx.compose.ui.unit.dp
|
|||
|
||||
val Shapes = Shapes(
|
||||
small = RoundedCornerShape(4.dp),
|
||||
medium = RoundedCornerShape(4.dp),
|
||||
medium = RoundedCornerShape(8.dp),
|
||||
large = RoundedCornerShape(0.dp)
|
||||
)
|
|
@ -5,7 +5,7 @@
|
|||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog.ui.theme
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material.MaterialTheme
|
||||
|
@ -14,19 +14,25 @@ import androidx.compose.material.lightColors
|
|||
import androidx.compose.runtime.Composable
|
||||
|
||||
private val DarkColorPalette = darkColors(
|
||||
primary = Purple200,
|
||||
primaryVariant = Purple700,
|
||||
secondary = Teal200
|
||||
primary = Orange,
|
||||
secondary = FocusBlueGray,
|
||||
background = BackgroundGray,
|
||||
surface = SurfaceDarkBlue,
|
||||
onSecondary = BackgroundGray,
|
||||
onBackground = PrimaryGold,
|
||||
)
|
||||
|
||||
private val LightColorPalette = lightColors(
|
||||
primary = Purple500,
|
||||
primaryVariant = Purple700,
|
||||
secondary = Teal200
|
||||
primary = Orange,
|
||||
secondary = FocusLightOrange,
|
||||
background = BackgroundLight,
|
||||
surface = SurfaceLight,
|
||||
onPrimary = PrimaryDarkGold,
|
||||
onBackground = PrimaryDarkGold,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun CatalogTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
|
||||
fun DualScreenExperienceTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
|
||||
val colors = if (darkTheme) {
|
||||
DarkColorPalette
|
||||
} else {
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.theme
|
||||
|
||||
import androidx.compose.material.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.microsoft.device.samples.dualscreenexperience.R
|
||||
|
||||
val DMSans = FontFamily(
|
||||
Font(R.font.dmsans_regular, FontWeight.Normal),
|
||||
Font(R.font.dmsans_medium, FontWeight.Medium),
|
||||
Font(R.font.dmsans_bold, FontWeight.Bold)
|
||||
)
|
||||
|
||||
val Roboto = FontFamily(Font(R.font.roboto, FontWeight.Normal))
|
||||
|
||||
val Typography = Typography(
|
||||
h2 = TextStyle(
|
||||
fontFamily = DMSans,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 24.sp
|
||||
),
|
||||
h3 = TextStyle(
|
||||
fontFamily = DMSans,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp,
|
||||
lineHeight = 28.sp,
|
||||
),
|
||||
h4 = TextStyle(
|
||||
fontFamily = DMSans,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 20.sp
|
||||
),
|
||||
h6 = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 18.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
body1 = TextStyle(
|
||||
fontFamily = DMSans,
|
||||
fontWeight = FontWeight.Normal,
|
||||
lineHeight = 24.sp,
|
||||
fontSize = 16.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
body2 = TextStyle(
|
||||
fontFamily = DMSans,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 18.sp,
|
||||
lineHeight = 27.sp
|
||||
),
|
||||
caption = TextStyle(
|
||||
fontFamily = DMSans,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp
|
||||
),
|
||||
subtitle1 = TextStyle(
|
||||
fontFamily = DMSans,
|
||||
fontWeight = FontWeight.Normal,
|
||||
lineHeight = 20.sp,
|
||||
fontSize = 13.sp
|
||||
),
|
||||
subtitle2 = TextStyle(
|
||||
fontFamily = DMSans,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
)
|
|
@ -25,9 +25,6 @@ import com.microsoft.device.samples.dualscreenexperience.domain.store.model.Stor
|
|||
import com.microsoft.device.samples.dualscreenexperience.presentation.product.util.StarRatingView
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.product.util.getProductContentDescription
|
||||
import com.microsoft.device.samples.dualscreenexperience.presentation.product.util.getProductDrawable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@BindingAdapter("storeImage")
|
||||
fun getStoreImageRes(view: ImageView, image: StoreImage?) {
|
||||
|
@ -89,20 +86,20 @@ fun invisibleIf(view: View, shouldBeInvisible: Boolean?) {
|
|||
|
||||
@BindingAdapter("price")
|
||||
fun formatPrice(view: TextView, value: Float) {
|
||||
val priceString = "$" + value.addThousandsSeparator()
|
||||
val priceString = value.toPriceString()
|
||||
view.text = priceString
|
||||
view.contentDescription = view.context.getString(R.string.price_with_label, priceString)
|
||||
}
|
||||
|
||||
@BindingAdapter("orderAmount")
|
||||
fun formatOrderAmount(view: TextView, value: Float) {
|
||||
val priceString = "$" + value.addThousandsSeparator()
|
||||
val priceString = value.toPriceString()
|
||||
view.text = view.context.getString(R.string.order_amount, priceString)
|
||||
}
|
||||
|
||||
@BindingAdapter("orderDate")
|
||||
fun formatOrderDate(view: TextView, value: Long) {
|
||||
val dateString = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()).format(Date(value))
|
||||
val dateString = value.toDateString()
|
||||
view.text = view.context.getString(R.string.order_date, dateString)
|
||||
}
|
||||
|
||||
|
|
|
@ -24,3 +24,7 @@ fun Float.addThousandsSeparator(): String =
|
|||
(NumberFormat.getInstance(Locale.getDefault()) as DecimalFormat).apply {
|
||||
applyPattern("#,###")
|
||||
}.format(this)
|
||||
|
||||
fun Float.toPriceString(): String {
|
||||
return "$" + addThousandsSeparator()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.util
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
fun Long.toDateString(): String {
|
||||
return SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()).format(Date(this))
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.presentation.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.microsoft.device.dualscreen.windowstate.WindowSizeClass
|
||||
import com.microsoft.device.dualscreen.windowstate.WindowState
|
||||
import com.microsoft.device.dualscreen.windowstate.getWindowSizeClass
|
||||
|
||||
@Composable
|
||||
fun WindowState.isLandscape(): Boolean {
|
||||
return when (foldIsSeparating) {
|
||||
true -> isDualLandscape()
|
||||
false -> isSingleLandscape()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WindowState.isExpanded(): Boolean {
|
||||
return when (foldIsSeparating) {
|
||||
true ->
|
||||
getWindowSizeClass(pane1SizeDp.width) == WindowSizeClass.EXPANDED ||
|
||||
getWindowSizeClass(pane2SizeDp.width) == WindowSizeClass.EXPANDED
|
||||
false -> widthSizeClass() == WindowSizeClass.EXPANDED
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WindowState.isSmallWidth(): Boolean {
|
||||
return when (isDualScreen()) {
|
||||
true -> pane1SizeDp.width.isSmallDimension() || pane2SizeDp.width.isSmallDimension()
|
||||
false -> windowWidthDp.isSmallDimension()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WindowState.isSmallHeight(): Boolean {
|
||||
return windowHeightDp.isSmallDimension()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Dp.isSmallDimension(): Boolean {
|
||||
return with(LocalDensity.current) { toPx() } < WIDTH_PX_BREAKPOINT
|
||||
}
|
|
@ -71,16 +71,16 @@ class TutorialBalloon @Inject constructor(val context: Context) {
|
|||
TutorialModel(balloonType).apply {
|
||||
when (balloonType) {
|
||||
TutorialBalloonType.LAUNCH_BOTTOM -> {
|
||||
backgroundDrawableIdRes = R.drawable.bottom_tutorial_balloon
|
||||
backgroundDrawableIdRes = R.drawable.bottom_left_tutorial_balloon
|
||||
stringRes = R.string.tutorial_launch_text
|
||||
}
|
||||
TutorialBalloonType.LAUNCH_RIGHT -> {
|
||||
backgroundDrawableIdRes = R.drawable.right_tutorial_balloon
|
||||
stringRes = R.string.tutorial_launch_text
|
||||
}
|
||||
TutorialBalloonType.STORES -> {
|
||||
backgroundDrawableIdRes = R.drawable.bottom_tutorial_balloon
|
||||
stringRes = R.string.tutorial_order_text
|
||||
TutorialBalloonType.HISTORY -> {
|
||||
backgroundDrawableIdRes = R.drawable.bottom_right_tutorial_balloon
|
||||
stringRes = R.string.tutorial_history_text
|
||||
}
|
||||
TutorialBalloonType.DEVELOPER_MODE -> {
|
||||
backgroundDrawableIdRes = R.drawable.top_tutorial_balloon
|
||||
|
@ -107,7 +107,7 @@ class TutorialBalloon @Inject constructor(val context: Context) {
|
|||
setPadding(halfTipPadding, microPadding, halfTipPadding, tipPadding)
|
||||
TutorialBalloonType.LAUNCH_RIGHT ->
|
||||
setPadding(halfTipPadding, halfTipPadding, tipPadding, halfTipPadding)
|
||||
TutorialBalloonType.STORES ->
|
||||
TutorialBalloonType.HISTORY ->
|
||||
setPadding(halfTipPadding, microPadding, halfTipPadding, tipPadding)
|
||||
TutorialBalloonType.DEVELOPER_MODE ->
|
||||
setPadding(halfTipPadding, tipPadding, halfTipPadding, halfTipPadding)
|
||||
|
@ -144,8 +144,8 @@ class TutorialBalloon @Inject constructor(val context: Context) {
|
|||
xOffset = parent.width
|
||||
yOffset = parent.height / 2 + ((tutorialContainer?.height ?: 0) / 2)
|
||||
}
|
||||
TutorialBalloonType.STORES -> {
|
||||
xOffset = parent.width / 2 - (tipHorizontalMargin + tipHeight / 2)
|
||||
TutorialBalloonType.HISTORY -> {
|
||||
xOffset -= (tutorialContainer?.width ?: 0) - (tipHorizontalMargin + parent.width / 2)
|
||||
}
|
||||
TutorialBalloonType.DEVELOPER_MODE -> {
|
||||
xOffset = parent.width / 2 - (tutorialContainer?.width ?: 0) + (tipHorizontalMargin + tipHeight / 2)
|
||||
|
@ -176,6 +176,6 @@ private data class TutorialModel(
|
|||
enum class TutorialBalloonType {
|
||||
LAUNCH_BOTTOM,
|
||||
LAUNCH_RIGHT,
|
||||
STORES,
|
||||
HISTORY,
|
||||
DEVELOPER_MODE
|
||||
}
|
||||
|
|
|
@ -17,11 +17,11 @@ import javax.inject.Inject
|
|||
class TutorialViewModel @Inject constructor(
|
||||
private val tutorialPrefs: TutorialPreferences
|
||||
) : ViewModel() {
|
||||
var showStoresTutorial = MutableLiveData<Boolean?>(null)
|
||||
var showHistoryTutorial = MutableLiveData<Boolean?>(null)
|
||||
|
||||
fun updateTutorial() {
|
||||
if (tutorialPrefs.shouldShowStoresTutorial()) {
|
||||
showStoresTutorial.value = true
|
||||
if (tutorialPrefs.shouldShowHistoryTutorial()) {
|
||||
showHistoryTutorial.value = true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,17 +29,17 @@ class TutorialViewModel @Inject constructor(
|
|||
tutorialPrefs.setShowLaunchTutorial(false)
|
||||
}
|
||||
|
||||
fun onStoresOpen() {
|
||||
if (showStoresTutorial.value != null) {
|
||||
tutorialPrefs.setShowStoresTutorial(false)
|
||||
fun onHistoryOpen() {
|
||||
if (showHistoryTutorial.value != null) {
|
||||
tutorialPrefs.setShowHistoryTutorial(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun shouldShowDeveloperModeTutorial() = tutorialPrefs.shouldShowDevModeTutorial()
|
||||
|
||||
fun shouldShowStoresTutorial() = tutorialPrefs.shouldShowStoresTutorial()
|
||||
|
||||
fun onDeveloperModeOpen() {
|
||||
tutorialPrefs.setShowDevModeTutorial(false)
|
||||
}
|
||||
|
||||
fun shouldShowDeveloperModeTutorial() = tutorialPrefs.shouldShowDevModeTutorial()
|
||||
|
||||
fun shouldShowHistoryTutorial() = tutorialPrefs.shouldShowHistoryTutorial()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~
|
||||
~ Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
~ Licensed under the MIT License.
|
||||
~
|
||||
-->
|
||||
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
|
||||
<item android:bottom="@dimen/tutorial_tip_height">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/tutorial_white" />
|
||||
<size
|
||||
android:width="@dimen/tutorial_balloon_width"
|
||||
android:height="40dp"/>
|
||||
<corners android:radius="@dimen/tutorial_balloon_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item android:gravity="right|bottom"
|
||||
android:right="@dimen/tutorial_tip_horizontal_margin">
|
||||
<rotate
|
||||
android:fromDegrees="45"
|
||||
android:pivotX="135%"
|
||||
android:toDegrees="45">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/tutorial_white" />
|
||||
<size
|
||||
android:width="@dimen/tutorial_tip_height"
|
||||
android:height="@dimen/tutorial_tip_height"/>
|
||||
</shape>
|
||||
</rotate>
|
||||
</item>
|
||||
</layer-list>
|
|
@ -0,0 +1,87 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="280dp"
|
||||
android:height="251dp"
|
||||
android:viewportWidth="280"
|
||||
android:viewportHeight="251">
|
||||
<path
|
||||
android:pathData="M75.38,114.75L75.6,192.27L143.05,153.31L142.84,75.8L75.38,114.75Z"
|
||||
android:fillColor="#997B55"/>
|
||||
<path
|
||||
android:pathData="M75.38,114.75L0,70.94L0.22,148.45L75.6,192.27L75.38,114.75Z"
|
||||
android:fillColor="#B99A6E"/>
|
||||
<path
|
||||
android:pathData="M142.84,75.8L67.46,32L0,70.94L75.38,114.75L142.84,75.8Z"
|
||||
android:fillColor="#D6B88C"/>
|
||||
<path
|
||||
android:pathData="M102.12,99.38L115.58,91.61V120.89L102.07,128.68L102.12,99.38Z"
|
||||
android:fillColor="#E6E6E6"/>
|
||||
<path
|
||||
android:pathData="M40.93,47.37L115.58,91.61L102.12,99.38L27.26,55.27L40.93,47.37Z"
|
||||
android:fillColor="#F2F2F2"/>
|
||||
<path
|
||||
android:pathData="M5.12,131.35L10.25,141.91L10.23,147.1L7.15,145.33V146.43L10.23,148.21L11.91,149.17L14.98,150.95V149.85L11.91,148.07L11.92,142.88L17.09,138.26L9.99,134.15L10.72,135.98L9.99,135.56L11.1,138.32L9.12,134.8L9.75,134.9L9.12,133.66L5.12,131.35Z"
|
||||
android:fillColor="#202020"/>
|
||||
<path
|
||||
android:pathData="M21.41,140.73L18.64,143.54L20.63,144.69L20.61,154.19L22.13,155.08L22.16,145.57L24.16,146.72L21.41,140.73Z"
|
||||
android:fillColor="#202020"/>
|
||||
<path
|
||||
android:pathData="M28.31,144.58L25.55,147.38L27.54,148.53L27.51,158.04L29.05,158.91L29.08,149.41L31.07,150.56L28.31,144.58Z"
|
||||
android:fillColor="#202020"/>
|
||||
<path
|
||||
android:pathData="M72.87,52.02L73.01,100.74L115.4,76.26L115.26,27.54L72.87,52.02Z"
|
||||
android:fillColor="#997B55"/>
|
||||
<path
|
||||
android:pathData="M72.87,52.02L25.48,24.48L25.62,73.2L73.01,100.74L72.87,52.02Z"
|
||||
android:fillColor="#B99A6E"/>
|
||||
<path
|
||||
android:pathData="M115.26,27.54L67.88,0L25.48,24.48L72.87,52.02L115.26,27.54Z"
|
||||
android:fillColor="#D6B88C"/>
|
||||
<path
|
||||
android:pathData="M89.66,42.34L98.12,37.47V55.86L89.64,60.77L89.66,42.34Z"
|
||||
android:fillColor="#E6E6E6"/>
|
||||
<path
|
||||
android:pathData="M51.21,9.66L98.12,37.47L89.66,42.34L42.62,14.63L51.21,9.66Z"
|
||||
android:fillColor="#F2F2F2"/>
|
||||
<path
|
||||
android:pathData="M28.69,62.44L31.92,69.08L31.91,72.35L29.98,71.23V71.93L31.91,73.04L32.96,73.65L34.89,74.76L34.9,74.07L32.97,72.96V69.69L36.22,66.78L31.77,64.21L32.22,65.36L31.75,65.1L32.45,66.82L31.21,64.61L31.61,64.68L31.21,63.89L28.69,62.44Z"
|
||||
android:fillColor="#202020"/>
|
||||
<path
|
||||
android:pathData="M38.94,68.35L37.2,70.11L38.45,70.82L38.43,76.8L39.4,77.36L39.41,71.38L40.67,72.1L38.94,68.35Z"
|
||||
android:fillColor="#202020"/>
|
||||
<path
|
||||
android:pathData="M43.28,70.76L41.54,72.52L42.79,73.25L42.78,79.21L43.74,79.77L43.75,73.79L45.01,74.52L43.28,70.76Z"
|
||||
android:fillColor="#202020"/>
|
||||
<path
|
||||
android:pathData="M179.56,182.83L179.79,250.2L251.14,209L250.91,141.64L179.56,182.83Z"
|
||||
android:fillColor="#997B55"/>
|
||||
<path
|
||||
android:pathData="M179.56,182.83L99.82,136.5L100.05,203.86L179.79,250.2L179.56,182.83Z"
|
||||
android:fillColor="#B99A6E"/>
|
||||
<path
|
||||
android:pathData="M250.91,141.64L171.16,95.3L99.82,136.5L179.56,182.83L250.91,141.64Z"
|
||||
android:fillColor="#BF9B6C"/>
|
||||
<path
|
||||
android:pathData="M105.23,185.76L110.66,196.94L110.64,202.43L107.39,200.56V201.72L110.64,203.6L112.42,204.62L115.67,206.5V205.33L112.42,203.46L112.43,197.97L117.9,193.08L110.39,188.75L111.16,190.68L110.39,190.22L111.55,193.14L109.46,189.42L110.14,189.54L109.47,188.21L105.23,185.76Z"
|
||||
android:fillColor="#202020"/>
|
||||
<path
|
||||
android:pathData="M122.46,195.69L119.54,198.66L121.65,199.88L121.61,209.94L123.23,210.87L123.26,200.81L125.37,202.03L122.46,195.69Z"
|
||||
android:fillColor="#202020"/>
|
||||
<path
|
||||
android:pathData="M129.77,199.76L126.85,202.73L128.95,203.93L128.92,213.99L130.55,214.93L130.57,204.87L132.68,206.09L129.77,199.76Z"
|
||||
android:fillColor="#202020"/>
|
||||
<path
|
||||
android:pathData="M99.99,136.43L73.49,78.86L147.51,36.82L171.27,95.3L99.99,136.43Z"
|
||||
android:fillColor="#A9885E"/>
|
||||
<path
|
||||
android:pathData="M171.27,95.3V178.21L179.49,183.02L250.76,141.91L171.27,95.3Z"
|
||||
android:fillColor="#CEA674"/>
|
||||
<path
|
||||
android:pathData="M171.27,95.3L197.76,36.82L280,83.42L250.76,141.91L171.27,95.3Z"
|
||||
android:fillColor="#997B55"/>
|
||||
<path
|
||||
android:pathData="M99.99,136.43L69.83,95.3L161.21,149.21L179.49,183.02L99.99,136.43Z"
|
||||
android:fillColor="#B78F5E"/>
|
||||
<path
|
||||
android:pathData="M179.49,183.02L205.08,147.38L279.09,104.44L250.76,141.91L179.49,183.02Z"
|
||||
android:fillColor="#B78F5E"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="25dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="25"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M19.55,12C19.55,7.996 16.304,4.75 12.3,4.75C10.638,4.75 9.106,5.309 7.884,6.25H8.55C9.102,6.25 9.55,6.698 9.55,7.25C9.55,7.802 9.102,8.25 8.55,8.25H5.55C4.998,8.25 4.55,7.802 4.55,7.25V7H4.516L4.55,6.948V4.25C4.55,3.698 4.998,3.25 5.55,3.25C6.102,3.25 6.55,3.698 6.55,4.25V4.754C8.129,3.499 10.127,2.75 12.3,2.75C17.409,2.75 21.55,6.891 21.55,12C21.55,17.109 17.409,21.25 12.3,21.25C7.191,21.25 3.05,17.109 3.05,12C3.05,11.617 3.073,11.24 3.118,10.87C3.181,10.358 3.634,10 4.15,10C4.741,10 5.167,10.568 5.099,11.156C5.067,11.433 5.05,11.714 5.05,12C5.05,16.004 8.296,19.25 12.3,19.25C16.304,19.25 19.55,16.004 19.55,12ZM13.3,8C13.3,7.448 12.852,7 12.3,7C11.748,7 11.3,7.448 11.3,8V13C11.3,13.552 11.748,14 12.3,14H15.3C15.852,14 16.3,13.552 16.3,13C16.3,12.448 15.852,12 15.3,12H13.3V8Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~
|
||||
~ Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
~ Licensed under the MIT License.
|
||||
~
|
||||
-->
|
||||
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".presentation.history.HistoryDetailFragment">
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/compose_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</layout>
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~
|
||||
~ Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
~ Licensed under the MIT License.
|
||||
~
|
||||
-->
|
||||
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".presentation.history.HistoryListFragment">
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/compose_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</layout>
|
|
@ -22,4 +22,8 @@
|
|||
<item android:id="@+id/navigation_orders_graph"
|
||||
android:icon="@drawable/ic_cart"
|
||||
android:title="@string/nav_order_title" />
|
||||
|
||||
<item android:id="@+id/navigation_history_graph"
|
||||
android:icon="@drawable/ic_history"
|
||||
android:title="@string/nav_history_title" />
|
||||
</menu>
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~
|
||||
~ Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
~ Licensed under the MIT License.
|
||||
~
|
||||
-->
|
||||
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/navigation_history_graph"
|
||||
app:startDestination="@id/fragment_history_list">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/fragment_history_list"
|
||||
android:name="com.microsoft.device.samples.dualscreenexperience.presentation.history.HistoryListFragment"
|
||||
android:label="Order History List"
|
||||
tools:layout="@layout/fragment_history_list">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_history_list_to_details"
|
||||
app:destination="@id/fragment_history_detail"
|
||||
app:launchScreen="end" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/fragment_history_detail"
|
||||
android:name="com.microsoft.device.samples.dualscreenexperience.presentation.history.HistoryDetailFragment"
|
||||
android:label="Order History Detail"
|
||||
tools:layout="@layout/fragment_history_detail" />
|
||||
|
||||
</navigation>
|
|
@ -15,5 +15,6 @@
|
|||
<include app:graph="@navigation/navigation_catalog_graph" />
|
||||
<include app:graph="@navigation/navigation_products_graph" />
|
||||
<include app:graph="@navigation/navigation_orders_graph" />
|
||||
<include app:graph="@navigation/navigation_history_graph" />
|
||||
|
||||
</navigation>
|
|
@ -22,12 +22,15 @@
|
|||
<string name="nav_catalog_title">Navigation Catalog</string>
|
||||
<string name="nav_products_title">Navigation Products</string>
|
||||
<string name="nav_order_title">Navigation Order</string>
|
||||
<string name="nav_history_title">Navigation History</string>
|
||||
|
||||
<string name="toolbar_stores_title">Stores</string>
|
||||
<string name="toolbar_catalog_title">New Guitars</string>
|
||||
<string name="toolbar_products_title">Product Details and Customization</string>
|
||||
<string name="toolbar_orders_title">Orders</string>
|
||||
<string name="toolbar_orders_receipt_title">Order receipt</string>
|
||||
<string name="toolbar_orders_receipt_title">Order Receipt</string>
|
||||
<string name="toolbar_history_title">Order History</string>
|
||||
<string name="toolbar_history_details_title">Order Details</string>
|
||||
|
||||
<string name="toolbar_dev_mode">Dev Mode</string>
|
||||
<string name="toolbar_dev_mode_design_pattern">Dev Mode - %s</string>
|
||||
|
@ -108,7 +111,6 @@
|
|||
<string name="order_quantity_minus">-</string>
|
||||
|
||||
<string name="order_success_message">Your order has been placed successfully!</string>
|
||||
<string name="tutorial_order_text">Check out more stores around you</string>
|
||||
|
||||
<string name="order_sign_cancel">Cancel</string>
|
||||
<string name="order_sign_confirm">Confirm</string>
|
||||
|
@ -131,6 +133,12 @@
|
|||
<string name="order_ink_stroke_medium">Medium</string>
|
||||
<string name="order_ink_stroke_large">Large</string>
|
||||
|
||||
<!-- Order History Mode -->
|
||||
|
||||
<string name="order_history_empty_message">There are no order items in your order history. Please head to cart to make new orders.</string>
|
||||
<string name="order_history_view">View</string>
|
||||
<string name="tutorial_history_text">Check out your first order in the history page</string>
|
||||
|
||||
<!-- Developer Mode -->
|
||||
|
||||
<string name="tutorial_developer_mode_text">Learn more about the design patterns and code</string>
|
||||
|
|
|
@ -25,6 +25,7 @@ class MockOrderDataSource : OrderDataSource {
|
|||
var autogeneratedOrderId = 0L
|
||||
|
||||
private val currentOrderLiveData = MutableLiveData<OrderWithItems?>()
|
||||
private val allSubmittedOrdersLiveData = MutableLiveData<List<OrderWithItems>>()
|
||||
|
||||
override suspend fun getAll(): List<OrderWithItems> = orderWithItemsEntityMap.values.toList()
|
||||
|
||||
|
@ -92,4 +93,11 @@ class MockOrderDataSource : OrderDataSource {
|
|||
Transformations.map(currentOrderLiveData) { orderWithItems ->
|
||||
orderWithItems?.takeIf { it.order.isSubmitted == submitted }
|
||||
}
|
||||
|
||||
override fun getAllSubmittedOrders(): LiveData<List<OrderWithItems>> {
|
||||
allSubmittedOrdersLiveData.value = orderWithItemsEntityMap.values.toList()
|
||||
return Transformations.map(allSubmittedOrdersLiveData) { orderWithItems ->
|
||||
orderWithItems.filter { it.order.isSubmitted }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,3 +72,7 @@ val orderWithItems = OrderWithItems(
|
|||
firstOrderEntity,
|
||||
mutableListOf(firstOrderItemEntity)
|
||||
)
|
||||
|
||||
val firstSubmittedOrder = firstOrder.copy(isSubmitted = true)
|
||||
|
||||
val firstSubmittedOrderEntity = firstOrderEntity.copy(isSubmitted = true)
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.microsoft.device.samples.dualscreenexperience.domain.order.usecases
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.testutil.MockOrderDataSource
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.testutil.firstOrderEntity
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.testutil.firstSubmittedOrder
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.testutil.firstSubmittedOrderEntity
|
||||
import com.microsoft.device.samples.dualscreenexperience.domain.order.testutil.getOrAwaitValue
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.hamcrest.MatcherAssert
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.hamcrest.core.Is.`is` as iz
|
||||
|
||||
class GetAllOrdersUseCaseTest {
|
||||
|
||||
private lateinit var getAllOrdersUseCase: GetAllSubmittedOrdersUseCase
|
||||
private lateinit var mockRepo: MockOrderDataSource
|
||||
|
||||
@get:Rule
|
||||
var instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockRepo = MockOrderDataSource()
|
||||
getAllOrdersUseCase = GetAllSubmittedOrdersUseCase(mockRepo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getEmptyListWhenNoOrders() = runBlocking {
|
||||
val resultValue = getAllOrdersUseCase.get().getOrAwaitValue()
|
||||
|
||||
MatcherAssert.assertThat(resultValue, iz(emptyList()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getEmptyListWhenNoSubmittedOrders() = runBlocking {
|
||||
val copyFirstOrderEntity = firstOrderEntity.copy()
|
||||
|
||||
mockRepo.insert(copyFirstOrderEntity)
|
||||
|
||||
val resultValue = getAllOrdersUseCase.get().getOrAwaitValue()
|
||||
|
||||
MatcherAssert.assertThat(resultValue, iz(emptyList()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getItemWhenSubmittedOrderExists() = runBlocking {
|
||||
val copyFirstSubmittedOrderEntity = firstSubmittedOrderEntity.copy()
|
||||
|
||||
mockRepo.insert(copyFirstSubmittedOrderEntity)
|
||||
|
||||
val resultValue = getAllOrdersUseCase.get().getOrAwaitValue()
|
||||
|
||||
MatcherAssert.assertThat(resultValue, iz(listOf(firstSubmittedOrder)))
|
||||
}
|
||||
}
|
|
@ -122,10 +122,11 @@ ext {
|
|||
|
||||
//UI dependencies
|
||||
glideVersion = "4.12.0"
|
||||
lottieVersion = '4.2.0'
|
||||
lottieVersion = '5.2.0'
|
||||
|
||||
uiDependencies = [
|
||||
lottie : "com.airbnb.android:lottie:$lottieVersion",
|
||||
lottieCompose : "com.airbnb.android:lottie-compose:$lottieVersion",
|
||||
glide : "com.github.bumptech.glide:glide:$glideVersion",
|
||||
glideAnnotationProcesor: "com.github.bumptech.glide:compiler:$glideVersion",
|
||||
]
|
||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 384 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 379 KiB |
Загрузка…
Ссылка в новой задаче