* 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:
Kristen Halper 2022-08-11 14:37:28 -07:00 коммит произвёл GitHub
Родитель b27b34168d
Коммит 76cf7dff05
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
62 изменённых файлов: 1872 добавлений и 163 удалений

Просмотреть файл

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

Просмотреть файл

@ -5,7 +5,7 @@
*
*/
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

Просмотреть файл

@ -5,8 +5,7 @@
*
*/
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

Просмотреть файл

@ -5,7 +5,7 @@
*
*/
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

Просмотреть файл

@ -5,7 +5,7 @@
*
*/
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

Просмотреть файл

@ -5,7 +5,7 @@
*
*/
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

Просмотреть файл

@ -5,7 +5,7 @@
*
*/
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

Просмотреть файл

@ -5,7 +5,7 @@
*
*/
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

Просмотреть файл

@ -5,7 +5,7 @@
*
*/
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",
]

Двоичные данные
screenshots/dual_portrait_history.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 384 KiB

Двоичные данные
screenshots/dual_portrait_history_light.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 379 KiB