diff --git a/Makefile b/Makefile index 2573eb31..b9777d55 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ buildWear: ./gradlew :wear:assemble $(params) buildDesktop: - ./gradlew :cmp-app:assemble $(params) + ./gradlew :cmp-common:jvmJar $(params) testCommon: ./gradlew :cmp-common:testDebugUnitTest $(params) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9093a61f..2a50cc7a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,6 +9,7 @@ plugins { id("com.android.application") id("com.autonomousapps.dependency-analysis") kotlin("android") + kotlin("plugin.serialization") id("org.jetbrains.kotlin.plugin.compose") id("com.google.protobuf") id("com.github.triplet.play") version "3.12.2" @@ -90,8 +91,9 @@ dependencies { implementation(libs.core.coreKtx) implementation(libs.material) - implementation(libs.compose.ui) - implementation(libs.compose.material3) + implementation(libs.cmp.ui) + implementation(libs.cmp.material3) + implementation(libs.cmp.components.resources) implementation(libs.compose.icons.core) debugImplementation(libs.compose.uiTooling) @@ -123,6 +125,10 @@ dependencies { implementation(libs.koin.android) implementation(libs.koin.android.compose) // lifecycleScope + // Decompose for navigation + implementation(libs.decompose.core) + implementation(libs.decompose.compose) + implementation(libs.vico.multiplatform) implementation(libs.vico.multiplatform.m3) diff --git a/app/src/main/java/ru/beryukhov/coffeegram/Application.kt b/app/src/main/java/ru/beryukhov/coffeegram/Application.kt index f6339636..f9df26dd 100644 --- a/app/src/main/java/ru/beryukhov/coffeegram/Application.kt +++ b/app/src/main/java/ru/beryukhov/coffeegram/Application.kt @@ -4,33 +4,18 @@ import android.app.Application import androidx.glance.appwidget.updateAll import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.koin.android.ext.android.get import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin -import org.koin.core.module.dsl.viewModel import org.koin.dsl.module -import ru.beryukhov.coffeegram.model.DaysCoffeesStore -import ru.beryukhov.coffeegram.model.DaysCoffeesStoreImpl -import ru.beryukhov.coffeegram.model.NavigationStore -import ru.beryukhov.coffeegram.model.ThemeState -import ru.beryukhov.coffeegram.model.ThemeStore -import ru.beryukhov.coffeegram.pages.AppWidgetViewModelImpl -import ru.beryukhov.coffeegram.pages.CoffeeListViewModelImpl -import ru.beryukhov.coffeegram.pages.MapPageViewModelImpl -import ru.beryukhov.coffeegram.pages.StatsPageViewModelImpl -import ru.beryukhov.coffeegram.pages.TablePageViewModelImpl -import ru.beryukhov.coffeegram.repository.CoffeeStorage -import ru.beryukhov.coffeegram.repository.ThemeDataStoreProtoStorage -import ru.beryukhov.coffeegram.store_lib.Storage +import ru.beryukhov.coffeegram.widget.DefaultWidgetDataBridge import ru.beryukhov.coffeegram.widget.FirstGlanceWidget +import ru.beryukhov.coffeegram.widget.WidgetDataBridge import ru.beryukhov.coffeegram.widget.setWidgetPreview import ru.beryukhov.repository.databaseModule -class Application : Application() { +open class Application : Application() { override fun onCreate() { super.onCreate() @@ -38,6 +23,7 @@ class Application : Application() { androidContext(this@Application) modules( appModule, + androidAppModule, databaseModule ) } @@ -46,31 +32,19 @@ class Application : Application() { MainScope().launch { withContext(Dispatchers.Default) { FirstGlanceWidget().updateAll(this@Application) - setWidgetPreview(this@Application) - get().state.onEach { - FirstGlanceWidget().updateAll(this@Application) - }.launchIn(this) + // if (not robolectric test) + setWidgetPreview() } } } -} -internal val appModule = module { - single> { - // ThemeSharedPrefStorage(context = context) - // ThemeDataStorePrefStorage(context = context) - ThemeDataStoreProtoStorage(context = get()) - } - single { - ThemeStore(get()) + open suspend fun setWidgetPreview() { + setWidgetPreview(this@Application) } - single { CoffeeStorage(get()) } - single { DaysCoffeesStoreImpl(get()) } -// single { LightDaysCoffeesStore() } - single { NavigationStore() } - viewModel { CoffeeListViewModelImpl(daysCoffeesStore = get(), navigationStore = get()) } - viewModel { TablePageViewModelImpl(daysCoffeesStore = get(), navigationStore = get()) } - viewModel { StatsPageViewModelImpl(daysCoffeesStore = get()) } - viewModel { MapPageViewModelImpl() } - viewModel { AppWidgetViewModelImpl(daysCoffeesStore = get()) } +} + +internal val androidAppModule = module { + // Theme storage and store + // Widget data bridge + single { DefaultWidgetDataBridge(daysCoffeesStore = get()) } } diff --git a/app/src/main/java/ru/beryukhov/coffeegram/MainActivity.kt b/app/src/main/java/ru/beryukhov/coffeegram/MainActivity.kt index 2e2f324e..dccfe70e 100644 --- a/app/src/main/java/ru/beryukhov/coffeegram/MainActivity.kt +++ b/app/src/main/java/ru/beryukhov/coffeegram/MainActivity.kt @@ -3,7 +3,6 @@ package ru.beryukhov.coffeegram import android.Manifest.permission.ACCESS_COARSE_LOCATION import android.content.pm.PackageManager import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.runtime.getValue @@ -11,121 +10,83 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.lifecycle.lifecycleScope -import com.google.android.gms.wearable.PutDataMapRequest -import com.google.android.gms.wearable.Wearable -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.launch -import kotlinx.coroutines.tasks.await -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import org.koin.compose.koinInject +import com.arkivanov.decompose.defaultComponentContext +import org.koin.android.ext.android.get import ru.beryukhov.coffeegram.animations.TransitionSlot +import ru.beryukhov.coffeegram.components.DefaultAndroidRootComponent import ru.beryukhov.coffeegram.data.CoffeeTypes -import ru.beryukhov.coffeegram.data.DAY_COFFEE_PATH import ru.beryukhov.coffeegram.data.DayCoffee -import ru.beryukhov.coffeegram.data.START_ACTIVITY_PATH -import ru.beryukhov.coffeegram.data.toDataMap -import ru.beryukhov.coffeegram.model.NavigationIntent -import ru.beryukhov.coffeegram.model.NavigationState.Companion.NAVIGATION_STATE_KEY -import ru.beryukhov.coffeegram.model.NavigationState.Companion.TODAYS_COFFEE_LIST -import ru.beryukhov.coffeegram.model.NavigationStore +import ru.beryukhov.coffeegram.model.DaysCoffeesStore +import ru.beryukhov.coffeegram.model.NavigationConstants.NAVIGATION_STATE_KEY +import ru.beryukhov.coffeegram.model.NavigationConstants.TODAYS_COFFEE_LIST +import ru.beryukhov.coffeegram.model.ThemeStore import ru.beryukhov.coffeegram.pages.LandingPage -import kotlin.time.Clock +import ru.beryukhov.coffeegram.screens.AndroidRootScreen +import ru.beryukhov.coffeegram.wearable.WearableSyncService import kotlin.time.ExperimentalTime @OptIn(ExperimentalTime::class) class MainActivity : ComponentActivity() { - internal val nodeClient by lazy { Wearable.getNodeClient(this) } - internal val messageClient by lazy { Wearable.getMessageClient(this) } - internal val dataClient by lazy { Wearable.getDataClient(this) } + private val wearableSyncService by lazy { WearableSyncService(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + val themeStore: ThemeStore = get() + val daysCoffeesStore: DaysCoffeesStore = get() + val showMap = checkCoarseLocationPermission() + + val rootComponent = DefaultAndroidRootComponent( + context = defaultComponentContext(), + themeStore = themeStore, + daysCoffeesStore = daysCoffeesStore, + showMap = showMap, + onStartWearableActivity = ::startWearableActivity, + onIconChange = { isSummer -> changeIcon(this, isSummer) }, + ) + setContent { var doAnimationState by rememberSaveable { mutableStateOf(true) } - val navigationStore: NavigationStore = koinInject() TransitionSlot( doAnimation = doAnimationState, StartPage = { modifier -> LandingPage(modifier = modifier) }, - EndPage = { modifier, topPadding -> - PagesContent( + EndPage = { modifier, _ -> + AndroidRootScreen( + rootComponent = rootComponent, modifier = modifier, - topPadding = topPadding, - startWearableActivity = ::startWearableActivity, - showMap = checkCoarseLocationPermission(), - navigationStore = navigationStore ) }, ) { doAnimationState = false - if (intent.getStringExtra(NAVIGATION_STATE_KEY) == TODAYS_COFFEE_LIST) { - navigationStore.newIntent( - NavigationIntent.OpenCoffeeListPage( - dayOfMonth = Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()).date.day - ) - ) - } + handleDeepLink(rootComponent) } } } -} - -private fun MainActivity.startWearableActivity() { - lifecycleScope.launch { - try { - val nodes = nodeClient.connectedNodes.await() // todo depending on nodes count show or hide button - // Send a message to all nodes in parallel - nodes.map { node -> - async { - messageClient.sendMessage(node.id, START_ACTIVITY_PATH, byteArrayOf()).await() - } - }.awaitAll() - - Log.d(TAG, "Starting activity requests sent successfully") - } catch (cancellationException: CancellationException) { - throw cancellationException - } catch (exception: Exception) { - Log.d(TAG, "Starting activity failed: $exception") + private fun handleDeepLink(rootComponent: DefaultAndroidRootComponent) { + if (intent.getStringExtra(NAVIGATION_STATE_KEY) == TODAYS_COFFEE_LIST) { + // Navigate to coffee list for today + // The CoffeeEditComponent handles the navigation to DayList internally + // We just need to ensure we're on the CoffeeEdit tab (index 0) + rootComponent.selectPage(0) } } - sendDayCoffee( - // todo replace mock - DayCoffee( + + private fun startWearableActivity() { + // Send mock data for now - in production this would use real data from store + val mockDayCoffee = DayCoffee( mapOf( CoffeeTypes.Cappuccino to 1, CoffeeTypes.Americano to 2 ) ) - ) -} - -private fun MainActivity.sendDayCoffee(dayCoffee: DayCoffee) { - lifecycleScope.launch { - try { - val request = PutDataMapRequest.create(DAY_COFFEE_PATH).apply { - dayCoffee.toDataMap(dataMap) - }.asPutDataRequest().setUrgent() - - val result = dataClient.putDataItem(request).await() - - Log.d(TAG, "DataItem saved: $result") - } catch (cancellationException: CancellationException) { - throw cancellationException - } catch (exception: Exception) { - Log.d(TAG, "Saving DataItem failed: $exception") - } + wearableSyncService.startWearableActivity(lifecycleScope, mockDayCoffee) } -} -private fun MainActivity.checkCoarseLocationPermission(): Boolean = checkSelfPermission( - ACCESS_COARSE_LOCATION -) == PackageManager.PERMISSION_GRANTED - -private const val TAG = "TestWatch_" + private fun checkCoarseLocationPermission(): Boolean = checkSelfPermission( + ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED +} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/PagesContent.kt b/app/src/main/java/ru/beryukhov/coffeegram/PagesContent.kt deleted file mode 100644 index 8d823e51..00000000 --- a/app/src/main/java/ru/beryukhov/coffeegram/PagesContent.kt +++ /dev/null @@ -1,161 +0,0 @@ -@file:OptIn(ExperimentalMaterial3Api::class) - -package ru.beryukhov.coffeegram - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import io.ktor.util.reflect.instanceOf -import org.koin.compose.koinInject -import ru.beryukhov.coffeegram.app_ui.CoffeegramTheme -import ru.beryukhov.coffeegram.model.NavigationState -import ru.beryukhov.coffeegram.model.NavigationStore -import ru.beryukhov.coffeegram.model.Text -import ru.beryukhov.coffeegram.model.ThemeState -import ru.beryukhov.coffeegram.model.ThemeStore -import ru.beryukhov.coffeegram.model.getNavBarItemsOld -import ru.beryukhov.coffeegram.pages.CoffeeListAppBar -import ru.beryukhov.coffeegram.pages.CoffeeListPage -import ru.beryukhov.coffeegram.pages.MapAppBar -import ru.beryukhov.coffeegram.pages.MapPage -import ru.beryukhov.coffeegram.pages.SettingsAppBar -import ru.beryukhov.coffeegram.pages.SettingsPage -import ru.beryukhov.coffeegram.pages.StatsAppBar -import ru.beryukhov.coffeegram.pages.StatsPage -import ru.beryukhov.coffeegram.pages.TableAppBar -import ru.beryukhov.coffeegram.pages.TablePage -import ru.beryukhov.date_time_utils.nowYM -import ru.beryukhov.date_time_utils.toTotalMonths - -@Preview(showBackground = true) -@Composable -internal fun DefaultPreview() { - PagesContent() -} - -@Composable -fun PagesContent( - modifier: Modifier = Modifier, - topPadding: Dp = 0.dp, - navigationStore: NavigationStore = koinInject(), - startWearableActivity: () -> Unit = {}, - showMap: Boolean = false -) { - val navigationState: NavigationState by navigationStore.state.collectAsState() - val currentNavigationState = navigationState - val snackbarHostState = remember { SnackbarHostState() } - - // You can scroll all the way up to the year 3000 with page count set to 36 000 --> (3000 * 12) - val pagerState = rememberPagerState( - pageCount = { 36_000 }, - initialPage = if (currentNavigationState is NavigationState.TablePage) { - currentNavigationState.yearMonth.toTotalMonths() - } else { - nowYM().toTotalMonths() - } - ) - - val navBarItems = remember(showMap) { getNavBarItemsOld(showMap) } - - CoffeegramTheme( - themeState = themeState() - ) { - Scaffold( - modifier, - contentWindowInsets = WindowInsets.systemBars, - topBar = { - when (currentNavigationState) { - is NavigationState.TablePage -> TableAppBar( - pagerState = pagerState - ) - - is NavigationState.CoffeeListPage -> CoffeeListAppBar( - localDate = currentNavigationState.date - ) - - is NavigationState.StatsPage -> StatsAppBar() - is NavigationState.SettingsPage -> SettingsAppBar() - - is NavigationState.MapPage -> MapAppBar() - } - }, - snackbarHost = { - SnackbarHost(hostState = snackbarHostState) - }, - bottomBar = { - NavigationBar { - navBarItems.forEach { item -> - NavigationBarItem( - selected = currentNavigationState.instanceOf(item.page), - onClick = { navigationStore.newIntent(item.intent) }, - label = { item.navBarItem.Text() }, - icon = { - Icon( - imageVector = item.navBarItem.icon, - contentDescription = "", - ) - } - ) - } - } - } - - ) { - Column( - modifier = Modifier - .padding(it) - .testTag(currentNavigationState.mapTestTag()) - ) { - Spacer( - Modifier - .padding(top = topPadding) - .align(Alignment.CenterHorizontally) - ) - when (currentNavigationState) { - is NavigationState.TablePage -> TablePage( - pagerState = pagerState - ) - - is NavigationState.CoffeeListPage -> CoffeeListPage( - localDate = currentNavigationState.date - ) - - is NavigationState.StatsPage -> StatsPage() - is NavigationState.SettingsPage -> SettingsPage( - themeStore = koinInject(), - snackbarHostState = snackbarHostState, - startWearableActivity = startWearableActivity, - ) - - is NavigationState.MapPage -> MapPage() - } - } - } - } -} - -@Composable -private fun themeState(): ThemeState { - val themeState: ThemeState by koinInject().state.collectAsState() - return themeState -} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/animations/AnimatedTextGradient.kt b/app/src/main/java/ru/beryukhov/coffeegram/animations/AnimatedTextGradient.kt deleted file mode 100644 index b32403a8..00000000 --- a/app/src/main/java/ru/beryukhov/coffeegram/animations/AnimatedTextGradient.kt +++ /dev/null @@ -1,98 +0,0 @@ -package ru.beryukhov.coffeegram.animations - -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.LinearGradientShader -import androidx.compose.ui.graphics.Shader -import androidx.compose.ui.graphics.ShaderBrush -import androidx.compose.ui.graphics.TileMode -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.tooling.preview.Preview - -@Composable -@Preview -private fun GradientTextPreview() { - AnimatedText(text = "Coffegram", modifier = Modifier) -} - -@Composable -internal fun AnimatedText(text: String, modifier: Modifier = Modifier) { - - val infiniteTransition = rememberInfiniteTransition() - val progressAnimation = infiniteTransition.animateFloat( - initialValue = -2f, - targetValue = 2f, - animationSpec = infiniteRepeatable( - tween( - durationMillis = 5000, - delayMillis = 10, - easing = LinearEasing - ) - ), - ) - - Box(modifier = modifier) { - Text( - text = text, - style = MaterialTheme.typography.titleLarge, - color = baseColor, - modifier = Modifier - .loadingRevealAnimation( - progress = progressAnimation - ), - ) - } -} - -private val color0 = Color(0x006F4E37) // Transparent Coffee Brown -private val color1 = Color(0x1A6F4E37) // Slightly visible Coffee Brown -private val color2 = Color(0x996F4E37) // Semi-transparent Coffee Brown -private val color3 = Color(0xFFC19A6B) // Solid Light Coffee -private val baseColor = Color(0xFF3B2F2F) // Solid Dark Coffee - -private fun Modifier.loadingRevealAnimation( - progress: State // -2 .. 2 -): Modifier = this - .graphicsLayer( - compositingStrategy = CompositingStrategy.Offscreen - ) - .drawWithContent { - drawContent() - drawRect( - brush = higherPickupFareGradient(progress), - blendMode = BlendMode.SrcAtop, - ) - } - -internal fun higherPickupFareGradient(progress: State): ShaderBrush { - val offset by progress - return object : ShaderBrush() { - override fun createShader(size: Size): Shader { - return LinearGradientShader( - colors = listOf(color0, color1, color2, color3, color2, color1, color0), - colorStops = listOf(0f, 0.05f, 0.3f, 0.5f, 0.7f, 0.95f, 1f), - // in design the gradient goes from (60,-1) to (10,9) on viewBox with size (58,27) - // we map it to the actual view size, then move it's horizontal zero point by offset from -2 to 2 widths - from = Offset(x = size.width * (60f / 58 + offset), y = size.height * (-1f / 27)), - to = Offset(x = size.width * (10f / 58 + offset), y = size.height * (9f / 27)), - tileMode = TileMode.Clamp - ) - } - } -} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/components/AndroidRootComponent.kt b/app/src/main/java/ru/beryukhov/coffeegram/components/AndroidRootComponent.kt new file mode 100644 index 00000000..aef9c38e --- /dev/null +++ b/app/src/main/java/ru/beryukhov/coffeegram/components/AndroidRootComponent.kt @@ -0,0 +1,120 @@ +package ru.beryukhov.coffeegram.components + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.router.pages.ChildPages +import com.arkivanov.decompose.router.pages.Pages +import com.arkivanov.decompose.router.pages.PagesNavigation +import com.arkivanov.decompose.router.pages.childPages +import com.arkivanov.decompose.router.pages.select +import com.arkivanov.decompose.value.Value +import kotlinx.coroutines.flow.StateFlow +import kotlinx.serialization.Serializable +import ru.beryukhov.coffeegram.model.DaysCoffeesStore +import ru.beryukhov.coffeegram.model.ThemeState +import ru.beryukhov.coffeegram.model.ThemeStore + +/** + * Root component for Android app with 4 tabs: + * - CoffeeEdit (Calendar + Day List) + * - Stats (Charts) + * - Map (Coffee shops, conditional on location permission) + * - Settings (Theme + Android-specific features) + */ +interface AndroidRootComponent { + val pages: Value> + val themeState: StateFlow + val showMap: Boolean + + fun selectPage(childIndex: Int) + + sealed interface Child { + class CoffeeEdit(val component: CoffeeEditComponent) : Child + class Stats(val component: StatsComponent) : Child + class Map(val component: MapComponent) : Child + class Settings(val component: AndroidSettingsComponent) : Child + } +} + +class DefaultAndroidRootComponent( + context: ComponentContext, + val themeStore: ThemeStore, + val daysCoffeesStore: DaysCoffeesStore, + override val showMap: Boolean, + private val onStartWearableActivity: () -> Unit, + private val onIconChange: (isSummer: Boolean) -> Unit, +) : AndroidRootComponent, ComponentContext by context { + + private val navigation = PagesNavigation() + + override val pages: Value> = + childPages( + source = navigation, + serializer = Config.serializer(), + initialPages = { + val items = buildList { + add(Config.CoffeeEdit) + add(Config.Stats) + if (showMap) add(Config.Map) + add(Config.Settings) + } + Pages(items = items, selectedIndex = 0) + }, + childFactory = ::child, + ) + + override val themeState: StateFlow = themeStore.state + + override fun selectPage(childIndex: Int) { + navigation.select(childIndex) + } + + private fun child( + config: Config, + context: ComponentContext, + ): AndroidRootComponent.Child = + when (config) { + Config.CoffeeEdit -> AndroidRootComponent.Child.CoffeeEdit( + DefaultCoffeeEditComponent( + context = context, + daysCoffeesStore = daysCoffeesStore, + ) + ) + + Config.Stats -> AndroidRootComponent.Child.Stats( + DefaultStatsComponent( + context = context, + daysCoffeesStore = daysCoffeesStore, + ) + ) + + Config.Map -> AndroidRootComponent.Child.Map( + DefaultMapComponent( + context = context, + ) + ) + + Config.Settings -> AndroidRootComponent.Child.Settings( + DefaultAndroidSettingsComponent( + context = context, + themeStore = themeStore, + onStartWearableActivity = onStartWearableActivity, + onIconChange = onIconChange, + ) + ) + } + + @Serializable + private sealed interface Config { + @Serializable + data object CoffeeEdit : Config + + @Serializable + data object Stats : Config + + @Serializable + data object Map : Config + + @Serializable + data object Settings : Config + } +} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/components/AndroidSettingsComponent.kt b/app/src/main/java/ru/beryukhov/coffeegram/components/AndroidSettingsComponent.kt new file mode 100644 index 00000000..9aeb41dd --- /dev/null +++ b/app/src/main/java/ru/beryukhov/coffeegram/components/AndroidSettingsComponent.kt @@ -0,0 +1,60 @@ +package ru.beryukhov.coffeegram.components + +import com.arkivanov.decompose.ComponentContext +import kotlinx.coroutines.flow.StateFlow +import ru.beryukhov.coffeegram.model.ThemeIntent +import ru.beryukhov.coffeegram.model.ThemeState +import ru.beryukhov.coffeegram.model.ThemeStore + +/** + * Extended settings component for Android-specific features. + * Adds wearable activity start and dynamic icon change capabilities. + */ +interface AndroidSettingsComponent : SettingsComponent { + fun onStartWearableActivity() + fun onIconChange(isSummer: Boolean) +} + +class DefaultAndroidSettingsComponent( + context: ComponentContext, + private val themeStore: ThemeStore, + private val onStartWearableActivity: () -> Unit, + private val onIconChange: (isSummer: Boolean) -> Unit, +) : AndroidSettingsComponent, ComponentContext by context { + + override val models: StateFlow = themeStore.state + + override fun onSetSystemTheme() { + themeStore.newIntent(ThemeIntent.SetSystemIntent) + } + + override fun onSetLightTheme() { + themeStore.newIntent(ThemeIntent.SetLightIntent) + } + + override fun onSetDarkTheme() { + themeStore.newIntent(ThemeIntent.SetDarkIntent) + } + + override fun onSetCupertinoTheme(enabled: Boolean) { + themeStore.newIntent(ThemeIntent.SetCupertinoIntent(enabled)) + } + + override fun onSetDynamicTheme(enabled: Boolean) { + themeStore.newIntent(ThemeIntent.SetDynamicIntent(enabled)) + } + + override fun onSetSummerTheme(enabled: Boolean) { + // Update the icon when summer theme is toggled + onIconChange(enabled) + themeStore.newIntent(ThemeIntent.SetSummerIntent(enabled)) + } + + override fun onStartWearableActivity() { + onStartWearableActivity.invoke() + } + + override fun onIconChange(isSummer: Boolean) { + onIconChange.invoke(isSummer) + } +} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/components/MapComponent.kt b/app/src/main/java/ru/beryukhov/coffeegram/components/MapComponent.kt new file mode 100644 index 00000000..d28725ed --- /dev/null +++ b/app/src/main/java/ru/beryukhov/coffeegram/components/MapComponent.kt @@ -0,0 +1,90 @@ +package ru.beryukhov.coffeegram.components + +import com.arkivanov.decompose.ComponentContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import ru.beryukhov.coffeegram.repository.CoffeeShop +import ru.beryukhov.coffeegram.repository.coffeeShops + +/** + * State for the map feature. + */ +data class CoffeeShopsState( + val list: List = emptyList(), + val expanded: Boolean = false, + val isLoading: Boolean = true, +) + +data class ExtendedCoffeeShop( + val coffeeShop: CoffeeShop, + val highlighted: Boolean = false, +) + +/** + * Component for the map feature showing coffee shop locations. + */ +interface MapComponent { + val coffeeShops: StateFlow + + fun onZoomChanged(zoom: Float) + fun onMarkerClicked(coffeeShop: CoffeeShop) +} + +class DefaultMapComponent( + context: ComponentContext, +) : MapComponent, ComponentContext by context { + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + + private val _coffeeShops = MutableStateFlow(CoffeeShopsState()) + override val coffeeShops: StateFlow = _coffeeShops.asStateFlow() + + init { + loadCoffeeShops() + } + + private fun loadCoffeeShops() { + scope.launch { + try { + val shops = coffeeShops() + _coffeeShops.value = CoffeeShopsState( + list = shops.map { ExtendedCoffeeShop(it, false) }, + expanded = false, + isLoading = false, + ) + } catch (e: Exception) { + _coffeeShops.value = CoffeeShopsState( + list = emptyList(), + expanded = false, + isLoading = false, + ) + } + } + } + + override fun onZoomChanged(zoom: Float) { + val newExpanded = zoom >= 11 + val current = _coffeeShops.value + if (current.expanded != newExpanded) { + _coffeeShops.value = current.copy(expanded = newExpanded) + } + } + + override fun onMarkerClicked(coffeeShop: CoffeeShop) { + val current = _coffeeShops.value + _coffeeShops.value = current.copy( + list = current.list.map { + if (it.coffeeShop == coffeeShop) { + it.copy(highlighted = true) + } else { + it.copy(highlighted = false) + } + } + ) + } +} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/components/StatsComponent.kt b/app/src/main/java/ru/beryukhov/coffeegram/components/StatsComponent.kt new file mode 100644 index 00000000..9b6a6dc3 --- /dev/null +++ b/app/src/main/java/ru/beryukhov/coffeegram/components/StatsComponent.kt @@ -0,0 +1,22 @@ +package ru.beryukhov.coffeegram.components + +import com.arkivanov.decompose.ComponentContext +import kotlinx.coroutines.flow.StateFlow +import ru.beryukhov.coffeegram.model.DaysCoffeesState +import ru.beryukhov.coffeegram.model.DaysCoffeesStore + +/** + * Component for statistics/charts feature. + * Wraps DaysCoffeesStore and exposes state for chart rendering. + */ +interface StatsComponent { + val models: StateFlow +} + +class DefaultStatsComponent( + context: ComponentContext, + private val daysCoffeesStore: DaysCoffeesStore, +) : StatsComponent, ComponentContext by context { + + override val models: StateFlow = daysCoffeesStore.state +} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/model/NavBarItemExt.kt b/app/src/main/java/ru/beryukhov/coffeegram/model/NavBarItemExt.kt deleted file mode 100644 index e6f6e802..00000000 --- a/app/src/main/java/ru/beryukhov/coffeegram/model/NavBarItemExt.kt +++ /dev/null @@ -1,39 +0,0 @@ -package ru.beryukhov.coffeegram.model - -import kotlinx.collections.immutable.persistentListOf -import kotlin.reflect.KClass - -internal data class NavBarItemExt( - val page: KClass, - val intent: NavigationIntent, - val navBarItem: NavBarItem -) -internal val calendarExt = NavBarItemExt( - NavigationState.TablePage::class, - NavigationIntent.ReturnToTablePage, - calendar -) - -internal val statsExt = NavBarItemExt( - NavigationState.StatsPage::class, - NavigationIntent.ToStatsPage, - stats -) - -internal val settingsExt = NavBarItemExt( - NavigationState.SettingsPage::class, - NavigationIntent.ToSettingsPage, - settings -) -internal val specialtyExt = NavBarItemExt( - NavigationState.MapPage::class, - NavigationIntent.ToMapPage, - specialty -) - -internal fun getNavBarItemsOld(showMap: Boolean) = - if (showMap) { - persistentListOf(calendarExt, statsExt, settingsExt, specialtyExt) - } else { - persistentListOf(calendarExt, statsExt, settingsExt) - } diff --git a/app/src/main/java/ru/beryukhov/coffeegram/model/NavigationConstants.kt b/app/src/main/java/ru/beryukhov/coffeegram/model/NavigationConstants.kt new file mode 100644 index 00000000..ce5a3ec3 --- /dev/null +++ b/app/src/main/java/ru/beryukhov/coffeegram/model/NavigationConstants.kt @@ -0,0 +1,9 @@ +package ru.beryukhov.coffeegram.model + +/** + * Navigation constants for deep linking and widget navigation. + */ +object NavigationConstants { + const val NAVIGATION_STATE_KEY = "NavigationState" + const val TODAYS_COFFEE_LIST = "TodaysCoffeeList" +} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/model/NavigationStore.kt b/app/src/main/java/ru/beryukhov/coffeegram/model/NavigationStore.kt deleted file mode 100644 index dec88f42..00000000 --- a/app/src/main/java/ru/beryukhov/coffeegram/model/NavigationStore.kt +++ /dev/null @@ -1,67 +0,0 @@ -package ru.beryukhov.coffeegram.model - -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.datetime.LocalDate -import ru.beryukhov.coffeegram.store_lib.StoreImpl -import ru.beryukhov.date_time_utils.YearMonth -import ru.beryukhov.date_time_utils.nowYM - -class NavigationStore(val yearMonth: YearMonth = nowYM()) : StoreImpl( - initialState = NavigationState.TablePage(yearMonth = yearMonth) -) { - - private val currentYearMonth = MutableStateFlow(nowYM()) - - override fun NavigationState.handleIntent(intent: NavigationIntent): NavigationState { - return when (intent) { - is NavigationIntent.OpenCoffeeListPage -> NavigationState.CoffeeListPage( - LocalDate( - year = currentYearMonth.value.year, - month = currentYearMonth.value.month, - day = intent.dayOfMonth - ) - ) - is NavigationIntent.SetYearMonth -> NavigationState.TablePage( - setYearMonth(yearMonth = intent.yearMonth) - ) - NavigationIntent.ReturnToTablePage -> NavigationState.TablePage(currentYearMonth.value) - NavigationIntent.ToStatsPage -> NavigationState.StatsPage - NavigationIntent.ToSettingsPage -> NavigationState.SettingsPage - NavigationIntent.ToMapPage -> NavigationState.MapPage - } - } - - private fun setYearMonth(yearMonth: YearMonth): YearMonth { - currentYearMonth.value = yearMonth - return currentYearMonth.value - } -} - -sealed interface NavigationIntent { - data class OpenCoffeeListPage(val dayOfMonth: Int) : NavigationIntent - data class SetYearMonth(val yearMonth: YearMonth) : NavigationIntent - data object ReturnToTablePage : NavigationIntent - data object ToStatsPage : NavigationIntent - data object ToSettingsPage : NavigationIntent - data object ToMapPage : NavigationIntent -} - -sealed interface NavigationState { - fun mapTestTag(): String = when (this) { - is CoffeeListPage -> "CoffeeListScreen" - MapPage -> "MapScreen" - StatsPage -> "StatsScreen" - SettingsPage -> "SettingsScreen" - is TablePage -> "TableScreen" - } - - class TablePage(val yearMonth: YearMonth) : NavigationState - data class CoffeeListPage(val date: LocalDate) : NavigationState - data object StatsPage : NavigationState - data object SettingsPage : NavigationState - data object MapPage : NavigationState - companion object { - const val NAVIGATION_STATE_KEY = "NavigationState" - const val TODAYS_COFFEE_LIST = "TodaysCoffeeList" - } -} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/pages/AppWidgetViewModel.kt b/app/src/main/java/ru/beryukhov/coffeegram/pages/AppWidgetViewModel.kt deleted file mode 100644 index 6cef9824..00000000 --- a/app/src/main/java/ru/beryukhov/coffeegram/pages/AppWidgetViewModel.kt +++ /dev/null @@ -1,95 +0,0 @@ -package ru.beryukhov.coffeegram.pages - -import androidx.lifecycle.ViewModel -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList -import kotlinx.datetime.TimeZone -import kotlinx.datetime.todayIn -import ru.beryukhov.coffeegram.data.CoffeeType -import ru.beryukhov.coffeegram.data.CoffeeTypeWithCount -import ru.beryukhov.coffeegram.data.CoffeeTypes -import ru.beryukhov.coffeegram.data.DayCoffee -import ru.beryukhov.coffeegram.data.withEmpty -import ru.beryukhov.coffeegram.model.DaysCoffeesIntent -import ru.beryukhov.coffeegram.model.DaysCoffeesState -import ru.beryukhov.coffeegram.model.DaysCoffeesStore -import kotlin.time.Clock -import kotlin.time.ExperimentalTime - -interface AppWidgetViewModel { - fun getCurrentDayCupsCount(): Int - fun getCurrentDayMostPopularWithCount(): CoffeeTypeWithCount - fun getCurrentDayList(): PersistentList - - fun currentDayIncrement(coffeeType: CoffeeType) - fun currentDayDecrement(coffeeType: CoffeeType) -} - -class AppWidgetViewModelStub : AppWidgetViewModel { - override fun getCurrentDayCupsCount(): Int = 0 - - override fun getCurrentDayMostPopularWithCount(): CoffeeTypeWithCount = - mockList.first() - - override fun getCurrentDayList(): PersistentList = - mockList - - override fun currentDayIncrement(coffeeType: CoffeeType) = Unit - - override fun currentDayDecrement(coffeeType: CoffeeType) = Unit -} - -private val mockList: PersistentList = persistentListOf( - CoffeeTypeWithCount(CoffeeTypes.Cappuccino, 5), - CoffeeTypeWithCount(CoffeeTypes.Espresso, 3), - CoffeeTypeWithCount(CoffeeTypes.Macchiato, 2), -) - -class AppWidgetViewModelImpl( - private val daysCoffeesStore: DaysCoffeesStore, -) : ViewModel(), AppWidgetViewModel { - - @OptIn(ExperimentalTime::class) - private fun getCurrentDay() = Clock.System.todayIn(TimeZone.currentSystemDefault()) - - override fun getCurrentDayCupsCount(): Int { - return getCurrentDayList().sumOf { it.count } - } - - override fun getCurrentDayMostPopularWithCount(): CoffeeTypeWithCount { - return getCurrentDayList().first() - } - - // coffee list does not contains 0 values and sorted by count - override fun getCurrentDayList(): PersistentList { - val dayCoffeeState: DaysCoffeesState = daysCoffeesStore.state.value - val dayCoffee = dayCoffeeState.coffees[getCurrentDay()] ?: DayCoffee() - val list = dayCoffee.coffeeCountMap.withEmpty().toList() - .sortedByDescending { it.count } - val emptyListMock = listOf(CoffeeTypeWithCount(CoffeeTypes.Cappuccino, 0)) - return list.toPersistentList() - } - - override fun currentDayDecrement(coffeeType: CoffeeType) { - newIntent( - DaysCoffeesIntent.MinusCoffee( - localDate = getCurrentDay(), - coffeeType = coffeeType - ) - ) - } - - override fun currentDayIncrement(coffeeType: CoffeeType) { - newIntent( - DaysCoffeesIntent.PlusCoffee( - localDate = getCurrentDay(), - coffeeType = coffeeType - ) - ) - } - - private fun newIntent(intent: DaysCoffeesIntent) { - daysCoffeesStore.newIntent(intent) - } -} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/pages/CoffeeListPage.kt b/app/src/main/java/ru/beryukhov/coffeegram/pages/CoffeeListPage.kt deleted file mode 100644 index 6f2ed183..00000000 --- a/app/src/main/java/ru/beryukhov/coffeegram/pages/CoffeeListPage.kt +++ /dev/null @@ -1,116 +0,0 @@ -package ru.beryukhov.coffeegram.pages - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.toPersistentList -import kotlinx.datetime.LocalDate -import org.koin.androidx.compose.koinViewModel -import ru.beryukhov.coffeegram.R -import ru.beryukhov.coffeegram.data.CoffeeType -import ru.beryukhov.coffeegram.data.CoffeeTypeWithCount -import ru.beryukhov.coffeegram.model.NavigationIntent -import ru.beryukhov.coffeegram.view.CoffeeTypeItem -import ru.beryukhov.date_time_utils.getFullMonthName - -@ExperimentalMaterial3Api -@Composable -fun CoffeeListAppBar( - localDate: LocalDate, - modifier: Modifier = Modifier, - coffeeListViewModel: CoffeeListViewModel = koinViewModel() -) { - TopAppBar( - modifier = modifier, - title = { - Text( - "${localDate.day} ${getFullMonthName(localDate.month).take(3)} " - + stringResource(R.string.add_drink) - ) - }, - navigationIcon = { - IconButton(onClick = { coffeeListViewModel.newIntent(NavigationIntent.ReturnToTablePage) }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "" - ) - } - } - ) -} - -@Composable -fun CoffeeListPage( - localDate: LocalDate, - modifier: Modifier = Modifier, - coffeeListViewModel: CoffeeListViewModel = koinViewModel() -) { - BackHandler { coffeeListViewModel.newIntent(NavigationIntent.ReturnToTablePage) } - val onPlusClick = remember(localDate, coffeeListViewModel) { - { coffeeType: CoffeeType -> - coffeeListViewModel.incrementCoffee(localDate, coffeeType) - } - } - val onMinusClick = remember(localDate, coffeeListViewModel) { - { coffeeType: CoffeeType -> - coffeeListViewModel.decrementCoffee(localDate, coffeeType) - } - } - CoffeeList( - coffeeItems = coffeeListViewModel.getDayCoffeesWithEmpty(localDate).toPersistentList(), - onPlusClick = onPlusClick, - onMinusClick = onMinusClick, - modifier = modifier - ) -} - -@Composable -private fun CoffeeList( - coffeeItems: PersistentList, - onPlusClick: (coffeeType: CoffeeType) -> Unit, - modifier: Modifier = Modifier, - onMinusClick: (coffeeType: CoffeeType) -> Unit -) { - LazyColumn( - modifier = modifier - .fillMaxHeight() - .lazyListLength(coffeeItems.size) - .testTag("CoffeeList") - ) { - itemsIndexed(items = coffeeItems) { index, model -> - val (type, count) = model - CoffeeTypeItem( - coffeeType = type, - count = count, - onIncrement = { onPlusClick(type) }, - onDecrement = { onMinusClick(type) }, - modifier = Modifier.lazyListItemPosition(index) - ) - } - } -} - -@Preview -@Composable -private fun Preview() { - CoffeeList( - coffeeItems = CoffeeListViewModelStub.getDayCoffeesWithEmpty(localDate = localDateStub), - onPlusClick = { }, - onMinusClick = { } - ) -} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/pages/CoffeeListViewModel.kt b/app/src/main/java/ru/beryukhov/coffeegram/pages/CoffeeListViewModel.kt deleted file mode 100644 index 490be50e..00000000 --- a/app/src/main/java/ru/beryukhov/coffeegram/pages/CoffeeListViewModel.kt +++ /dev/null @@ -1,93 +0,0 @@ -package ru.beryukhov.coffeegram.pages - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.lifecycle.ViewModel -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.toPersistentList -import kotlinx.datetime.LocalDate -import ru.beryukhov.coffeegram.data.CoffeeType -import ru.beryukhov.coffeegram.data.CoffeeTypeWithCount -import ru.beryukhov.coffeegram.data.DayCoffee -import ru.beryukhov.coffeegram.data.withEmpty -import ru.beryukhov.coffeegram.model.DaysCoffeesIntent -import ru.beryukhov.coffeegram.model.DaysCoffeesState -import ru.beryukhov.coffeegram.model.DaysCoffeesStore -import ru.beryukhov.coffeegram.model.NavigationIntent -import ru.beryukhov.coffeegram.model.NavigationStore -import ru.beryukhov.date_time_utils.nowLD - -interface CoffeeListViewModel { - @Composable - fun getDayCoffeesWithEmpty(localDate: LocalDate): PersistentList - - fun decrementCoffee( - localDate: LocalDate, - coffeeType: CoffeeType - ) - - fun incrementCoffee( - localDate: LocalDate, - coffeeType: CoffeeType - ) - - fun newIntent(intent: NavigationIntent) -} - -object CoffeeListViewModelStub : CoffeeListViewModel { - override fun newIntent(intent: NavigationIntent) = Unit - - override fun decrementCoffee(localDate: LocalDate, coffeeType: CoffeeType) = Unit - override fun incrementCoffee(localDate: LocalDate, coffeeType: CoffeeType) = Unit - - @Composable - override fun getDayCoffeesWithEmpty(localDate: LocalDate): PersistentList = - emptyMap().withEmpty().toPersistentList() -} - -val localDateStub: LocalDate = nowLD() - -class CoffeeListViewModelImpl( - private val daysCoffeesStore: DaysCoffeesStore, - private val navigationStore: NavigationStore -) : ViewModel(), CoffeeListViewModel { - @Composable - override fun getDayCoffeesWithEmpty(localDate: LocalDate): PersistentList { - val dayCoffeeState: DaysCoffeesState by daysCoffeesStore.state.collectAsState() - val dayCoffee = dayCoffeeState.coffees[localDate] ?: DayCoffee() - return dayCoffee.coffeeCountMap.withEmpty().toPersistentList() - } - - override fun decrementCoffee( - localDate: LocalDate, - coffeeType: CoffeeType - ) { - newIntent( - DaysCoffeesIntent.MinusCoffee( - localDate = localDate, - coffeeType = coffeeType - ) - ) - } - - override fun incrementCoffee( - localDate: LocalDate, - coffeeType: CoffeeType - ) { - newIntent( - DaysCoffeesIntent.PlusCoffee( - localDate = localDate, - coffeeType = coffeeType - ) - ) - } - - private fun newIntent(intent: DaysCoffeesIntent) { - daysCoffeesStore.newIntent(intent) - } - - override fun newIntent(intent: NavigationIntent) { - navigationStore.newIntent(intent) - } -} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/pages/MapPage.kt b/app/src/main/java/ru/beryukhov/coffeegram/pages/MapPage.kt deleted file mode 100644 index 2f35f65a..00000000 --- a/app/src/main/java/ru/beryukhov/coffeegram/pages/MapPage.kt +++ /dev/null @@ -1,339 +0,0 @@ -@file:OptIn(ExperimentalMaterial3Api::class) - -package ru.beryukhov.coffeegram.pages - -import android.Manifest -import android.content.Context -import android.content.pm.PackageManager -import android.location.LocationManager -import android.util.Log -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Place -import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.google.android.gms.maps.CameraUpdate -import com.google.android.gms.maps.CameraUpdateFactory -import com.google.android.gms.maps.model.LatLng -import com.google.android.gms.maps.model.LatLngBounds -import com.google.maps.android.compose.GoogleMap -import com.google.maps.android.compose.MapProperties -import com.google.maps.android.compose.MapUiSettings -import com.google.maps.android.compose.MarkerComposable -import com.google.maps.android.compose.MarkerState -import com.google.maps.android.compose.rememberCameraPositionState -import com.google.maps.android.ktx.model.cameraPosition -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import org.koin.androidx.compose.koinViewModel -import ru.beryukhov.coffeegram.R -import ru.beryukhov.coffeegram.repository.CoffeeShop -import ru.beryukhov.coffeegram.repository.coffeeShops -import ru.beryukhov.coffeegram.repository.latlng - -@Composable -internal fun MapPagePreview() { - Column { - MapPage() - } -} - -@Composable -fun ColumnScope.MapPage(modifier: Modifier = Modifier) { - val context = LocalContext.current - val coarseLocationEnabled = remember { - context.checkSelfPermission( - Manifest.permission.ACCESS_COARSE_LOCATION - ) == PackageManager.PERMISSION_GRANTED - } - val coarseLocation = remember { - val locationDefault = LatLng(35.1272, 33.3371) - val coarseLocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager - try { - coarseLocationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) - ?.let { LatLng(it.latitude, it.longitude) } ?: locationDefault - } catch (_: SecurityException) { - locationDefault - } - } - - val viewModel = koinViewModel() - - if (coarseLocationEnabled) { - val coffeeShops by viewModel.coffeeShops.collectAsState() - - val cameraPositionState = rememberCameraPositionState { - position = cameraPosition { - target(coarseLocation) - zoom(10f) - } - } - LaunchedEffect(cameraPositionState.position.zoom) { - viewModel.newZoom(cameraPositionState.position.zoom) - } - Box( - modifier = Modifier.weight(1f), - ) { - GoogleMap( - modifier = Modifier.fillMaxSize(), - properties = MapProperties().copy( - isMyLocationEnabled = true - ), - uiSettings = MapUiSettings( - compassEnabled = false, - zoomControlsEnabled = false, - myLocationButtonEnabled = false - ), - cameraPositionState = cameraPositionState - ) { - coffeeShops.list.forEach { - MarkerComposable( - keys = arrayOf(it.highlighted, coffeeShops.expanded), - state = MarkerState(it.coffeeShop.latlng()), - onClick = { _ -> - viewModel.markerClick(it.coffeeShop) - true - }, - zIndex = if (it.highlighted) 1f else 0f - ) { - Marker( - name = it.coffeeShop.name, - descr = it.coffeeShop.description, - highlighted = it.highlighted, - expanded = coffeeShops.expanded, - ) - } - } - } - - val density = LocalDensity.current.density - Button( - onClick = { - panMapToFitAllMarkers(coffeeShops.list.map { it.coffeeShop.latlng() }, density)?.let { - cameraPositionState.move( - it - ) - } - }, - modifier = Modifier.padding(16.dp) - ) { - Icon( - imageVector = Icons.Rounded.Place, - contentDescription = "" - ) - } - } - } else { - Box( - modifier = modifier.fillMaxSize(), - ) { - Text(text = "Your should give location permission", modifier = Modifier.align(Alignment.Center)) - } - } -} - -private fun Dp.toPx(density: Float): Int { - return (value * density).toInt() -} - -fun panMapToFitAllMarkers(locations: List, density: Float): CameraUpdate? = - when { - locations.isEmpty() -> { - null - } - locations.size == 1 -> { - CameraUpdateFactory.newCameraPosition(cameraPosition { - target(locations.first()) - }) - } - else -> { - val latLng = LatLngBounds.builder().apply { - locations.forEach { include(it) } - }.build() - CameraUpdateFactory.newLatLngBounds( - /* bounds = */ latLng, - /* padding = */ 72.dp.toPx(density) - ) - } - } - -@Composable -@Preview -private fun SmallMarker() = Marker(expanded = false) - -@Composable -@Preview -private fun ExpandedMarker() = Marker(expanded = true) - -@Composable -fun Marker( - modifier: Modifier = Modifier, - name: String = "Title", - descr: String = "Subtitle", - highlighted: Boolean = false, - expanded: Boolean = false, -) { - val borderRadius = if (expanded) 12.dp else 6.dp - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .padding(16.dp) - .boxShadow( - blurRadius = 3.dp, - offset = DpOffset(x = 0.dp, y = 2.dp), - shape = RoundedCornerShape(borderRadius), - color = Color(0f, 0f, 0f, 0.15f) - ) - .boxShadow( - blurRadius = 9.dp, - offset = DpOffset(x = 0.dp, y = 6.dp), - shape = RoundedCornerShape(borderRadius), - color = Color(0f, 0f, 0f, 0.04f) - ) -// .height(if (expanded) 44.dp else 30.dp) - .background( - color = if (highlighted) Color(0xFFE8E5E3) else Color(0xFFFFFFFF), - shape = RoundedCornerShape(size = 6.dp) - ) - .padding(start = 4.dp, top = 3.dp, end = 8.dp, bottom = 3.dp) - .widthIn(min = 0.dp, max = (LocalConfiguration.current.screenWidthDp / if (highlighted) 1 else 2).dp) - - ) { - Image( - painter = painterResource(id = R.drawable.logo_splash), - contentDescription = "image description", - contentScale = ContentScale.Fit, - modifier = Modifier - .padding(start = 0.dp, top = 1.dp, end = 2.dp, bottom = 1.dp) - .width(18.dp) - .height(18.dp) - - ) - Column( - verticalArrangement = Arrangement.spacedBy(if (highlighted) 4.dp else -4.dp, Alignment.CenterVertically), - horizontalAlignment = Alignment.Start, - modifier = Modifier.padding(start = 4.dp) - ) { - val textColor = /*Color(0xFF003DD9)*/ MaterialTheme.colorScheme.onPrimaryContainer - Text( - text = name, - style = TextStyle( - fontSize = 17.sp, - lineHeight = 24.sp, - fontWeight = FontWeight(350), - color = textColor, - ), - maxLines = if (highlighted) 2 else 1, - overflow = TextOverflow.Ellipsis - ) - if (expanded) { - Text( - text = descr, - style = TextStyle( - fontSize = 13.sp, - lineHeight = 18.sp, - fontWeight = FontWeight(350), - color = textColor, - ), - maxLines = if (highlighted) 3 else 1, - overflow = TextOverflow.Ellipsis - ) - } - } - } -} - -@Composable -fun MapAppBar(modifier: Modifier = Modifier) { - TopAppBar( - title = { Text(stringResource(R.string.map_long)) }, - modifier = modifier - ) -} - -interface MapPageViewModel { - val coffeeShops: StateFlow - - fun newZoom(zoom: Float) - fun markerClick(coffeeShop: CoffeeShop) -} - -class MapPageViewModelImpl : ViewModel(), MapPageViewModel { - override val coffeeShops: MutableStateFlow by lazy { - val m = MutableStateFlow(CoffeeShopsState(emptyList(), false)) - viewModelScope.launch { - m.tryEmit(CoffeeShopsState(coffeeShops().map { ExtendedCoffeeShop(it, false) }, false)) - } - m - } - - override fun newZoom(zoom: Float) { - Log.d("MapPageViewModelImpl", "newZoom: $zoom") - val newExpanded = zoom >= 11 - if (coffeeShops.value.expanded != newExpanded) { - coffeeShops.tryEmit(coffeeShops.value.copy(expanded = newExpanded)) - } - } - - override fun markerClick(coffeeShop: CoffeeShop) { - Log.d("MapPageViewModelImpl", "markerClick: $coffeeShop") - coffeeShops.tryEmit(coffeeShops.value.copy(list = coffeeShops.value.list.map { - if (it.coffeeShop == coffeeShop) { - it.copy(highlighted = true) - } else { - it.copy(highlighted = false) - } - })) - } -} - -data class CoffeeShopsState( - val list: List, - val expanded: Boolean -) - -data class ExtendedCoffeeShop( - val coffeeShop: CoffeeShop, - val highlighted: Boolean, -) diff --git a/app/src/main/java/ru/beryukhov/coffeegram/pages/StatsPage.kt b/app/src/main/java/ru/beryukhov/coffeegram/pages/StatsPage.kt deleted file mode 100644 index 0a7c569b..00000000 --- a/app/src/main/java/ru/beryukhov/coffeegram/pages/StatsPage.kt +++ /dev/null @@ -1,62 +0,0 @@ -@file:OptIn(ExperimentalMaterial3Api::class) - -package ru.beryukhov.coffeegram.pages - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.lifecycle.ViewModel -import org.koin.androidx.compose.koinViewModel -import ru.beryukhov.coffeegram.R -import ru.beryukhov.coffeegram.model.DaysCoffeesState -import ru.beryukhov.coffeegram.model.DaysCoffeesStore - -@Composable -fun ColumnScope.StatsPage( - modifier: Modifier = Modifier, - statsPageViewModel: StatsPageViewModel = koinViewModel(), -) { - CoffeeCharts(coffeeState = statsPageViewModel.getState(), modifier = modifier.weight(1f)) -} - -@Preview -@Composable -private fun Preview() { - Column { - StatsPage( - statsPageViewModel = StatsPageViewModelStub, - ) - } -} - -@Composable -fun StatsAppBar(modifier: Modifier = Modifier) { - TopAppBar( - title = { Text(stringResource(R.string.stats)) }, - modifier = modifier - ) -} - -interface StatsPageViewModel { - @Composable - fun getState(): DaysCoffeesState -} - -object StatsPageViewModelStub : StatsPageViewModel { - @Composable - override fun getState() = DaysCoffeesState() -} - -class StatsPageViewModelImpl( - private val daysCoffeesStore: DaysCoffeesStore, -) : ViewModel(), StatsPageViewModel { - @Composable - override fun getState(): DaysCoffeesState = daysCoffeesStore.state.collectAsState().value -} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/pages/TablePage.kt b/app/src/main/java/ru/beryukhov/coffeegram/pages/TablePage.kt deleted file mode 100644 index 0fcf6151..00000000 --- a/app/src/main/java/ru/beryukhov/coffeegram/pages/TablePage.kt +++ /dev/null @@ -1,167 +0,0 @@ -@file:OptIn(ExperimentalTime::class) - -package ru.beryukhov.coffeegram.pages - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PagerState -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.ParagraphStyle -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -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 kotlinx.collections.immutable.toPersistentMap -import kotlinx.coroutines.launch -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import org.koin.androidx.compose.koinViewModel -import ru.beryukhov.coffeegram.R -import ru.beryukhov.coffeegram.model.NavigationIntent -import ru.beryukhov.coffeegram.view.MonthTable -import ru.beryukhov.date_time_utils.getFullMonthName -import ru.beryukhov.date_time_utils.toYearMonth -import kotlin.time.Clock -import kotlin.time.ExperimentalTime - -@ExperimentalMaterial3Api -@Composable -fun TableAppBar( - pagerState: PagerState, - modifier: Modifier = Modifier -) { - val scope = rememberCoroutineScope() - val yearMonth = pagerState.currentPage.toYearMonth() - - TopAppBar( - modifier = modifier, - title = { - Row(horizontalArrangement = Arrangement.Center) { - Text( - modifier = Modifier.weight(1f).testTag("Month"), - text = AnnotatedString( - text = getFullMonthName(yearMonth.month), - paragraphStyle = ParagraphStyle(textAlign = TextAlign.Center) - ) - ) - } - }, - navigationIcon = { - IconButton( - onClick = { - scope.launch { - pagerState.animateScrollToPage(pagerState.currentPage - 1) - } - }, - modifier = Modifier.semantics { - contentDescription = "LeftArrow" - } - ) { Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft, contentDescription = "") } - }, - actions = { - IconButton( - onClick = { - scope.launch { - pagerState.animateScrollToPage(pagerState.currentPage + 1) - } - }, - modifier = Modifier.testTag("RightArrow") - ) { Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "") } - } - ) -} - -@Composable -fun ColumnScope.TablePage( - pagerState: PagerState, - modifier: Modifier = Modifier, - tablePageViewModel: TablePageViewModel = koinViewModel(), -) { - - val yearMonth = pagerState.currentPage.toYearMonth() - - LaunchedEffect(key1 = pagerState.currentPage) { - tablePageViewModel.newIntent(NavigationIntent.SetYearMonth(pagerState.currentPage.toYearMonth())) - } - - Column(horizontalAlignment = Alignment.End, modifier = modifier.weight(1f).testTag("TableScreen")) { - HorizontalPager(state = pagerState) { - MonthTable( - yearMonth = yearMonth, - today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date, - filledDayItemsMap = tablePageViewModel.getFilledDayItemsMap(yearMonth).toPersistentMap(), - onClick = { dayOfMonth: Int -> - tablePageViewModel.newIntent( - NavigationIntent.OpenCoffeeListPage( - dayOfMonth - ) - ) - }, - modifier = Modifier.wrapContentHeight() - ) - } - Row( - modifier = Modifier.fillMaxSize(), - ) { - LottieCoffee(modifier = Modifier.weight(1f, fill = false), alignment = Alignment.BottomStart) - Text( - "${yearMonth.year}", - modifier = Modifier - .padding(16.dp) - .align(Alignment.Bottom) - .testTag("Year") - ) - } - } -} - -@Preview -@Composable -private fun Preview() { - Column { - val pagerState = rememberPagerState(pageCount = { 36_000 }) - TablePage( - tablePageViewModel = TablePageViewModelStub, - pagerState = pagerState - ) - } -} - -@Composable -fun LottieCoffee(modifier: Modifier = Modifier, alignment: Alignment = Alignment.Center) { - val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.lottie_coffee)) - val progress by animateLottieCompositionAsState(composition) - LottieAnimation( - composition = composition, - progress = { progress }, - modifier = modifier, - alignment = alignment, - ) -} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/pages/TablePageViewModel.kt b/app/src/main/java/ru/beryukhov/coffeegram/pages/TablePageViewModel.kt deleted file mode 100644 index ced6b28c..00000000 --- a/app/src/main/java/ru/beryukhov/coffeegram/pages/TablePageViewModel.kt +++ /dev/null @@ -1,48 +0,0 @@ -package ru.beryukhov.coffeegram.pages - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.lifecycle.ViewModel -import kotlinx.datetime.LocalDate -import ru.beryukhov.coffeegram.data.DayCoffee -import ru.beryukhov.coffeegram.data.Picture -import ru.beryukhov.coffeegram.data.getDayIconCoffeeType -import ru.beryukhov.coffeegram.model.DaysCoffeesStore -import ru.beryukhov.coffeegram.model.NavigationIntent -import ru.beryukhov.coffeegram.model.NavigationStore -import ru.beryukhov.date_time_utils.YearMonth - -interface TablePageViewModel { - @Composable - fun getFilledDayItemsMap(yearMonth: YearMonth): Map - - fun newIntent(intent: NavigationIntent) -} - -object TablePageViewModelStub : TablePageViewModel { - @Composable - override fun getFilledDayItemsMap(yearMonth: YearMonth): Map = emptyMap() - override fun newIntent(intent: NavigationIntent) = Unit -} - -class TablePageViewModelImpl( - private val daysCoffeesStore: DaysCoffeesStore, - private val navigationStore: NavigationStore -) : ViewModel(), TablePageViewModel { - @Composable - override fun getFilledDayItemsMap(yearMonth: YearMonth): Map { - val coffeesState by daysCoffeesStore.state.collectAsState() - return coffeesState.coffees - .filter { entry: Map.Entry -> - entry.key.year == yearMonth.year - && entry.key.month == yearMonth.month - } - .mapKeys { entry: Map.Entry -> entry.key.day } - .mapValues { entry: Map.Entry -> entry.value.getDayIconCoffeeType() } - } - - override fun newIntent(intent: NavigationIntent) { - navigationStore.newIntent(intent) - } -} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/pages/WidgetDataBridgeStub.kt b/app/src/main/java/ru/beryukhov/coffeegram/pages/WidgetDataBridgeStub.kt new file mode 100644 index 00000000..d3026cd3 --- /dev/null +++ b/app/src/main/java/ru/beryukhov/coffeegram/pages/WidgetDataBridgeStub.kt @@ -0,0 +1,30 @@ +package ru.beryukhov.coffeegram.pages + +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import ru.beryukhov.coffeegram.data.CoffeeType +import ru.beryukhov.coffeegram.data.CoffeeTypeWithCount +import ru.beryukhov.coffeegram.data.CoffeeTypes +import ru.beryukhov.coffeegram.widget.WidgetDataBridge + +object WidgetDataBridgeStub : WidgetDataBridge { + override fun getCurrentDayCupsCount(): Flow = flowOf(0) + + override fun getCurrentDayMostPopularWithCount(): Flow = + flowOf(mockList.first()) + + override fun getCurrentDayList(): Flow> = + flowOf(mockList) + + override fun incrementCoffee(coffeeType: CoffeeType) = Unit + + override fun decrementCoffee(coffeeType: CoffeeType) = Unit +} + +private val mockList: PersistentList = persistentListOf( + CoffeeTypeWithCount(CoffeeTypes.Cappuccino, 5), + CoffeeTypeWithCount(CoffeeTypes.Espresso, 3), + CoffeeTypeWithCount(CoffeeTypes.Macchiato, 2), +) diff --git a/app/src/main/java/ru/beryukhov/coffeegram/screens/AndroidRootScreen.kt b/app/src/main/java/ru/beryukhov/coffeegram/screens/AndroidRootScreen.kt new file mode 100644 index 00000000..10dde426 --- /dev/null +++ b/app/src/main/java/ru/beryukhov/coffeegram/screens/AndroidRootScreen.kt @@ -0,0 +1,128 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package ru.beryukhov.coffeegram.screens + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.pages.ChildPages +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import org.jetbrains.compose.resources.stringResource +import ru.beryukhov.coffeegram.app_ui.CoffeegramTheme +import ru.beryukhov.coffeegram.components.AndroidRootComponent +import ru.beryukhov.coffeegram.model.NavBarItem +import ru.beryukhov.coffeegram.model.calendar +import ru.beryukhov.coffeegram.model.settings +import ru.beryukhov.coffeegram.model.specialty +import ru.beryukhov.coffeegram.model.stats +import ru.beryukhov.coffeegram.screens.CoffeeEditAppBar as CmpCoffeeEditAppBar +import ru.beryukhov.coffeegram.screens.CoffeeEditScreen as CmpCoffeeEditScreen + +@Composable +fun AndroidRootScreen( + rootComponent: AndroidRootComponent, + modifier: Modifier = Modifier, +) { + val snackbarHostState = remember { SnackbarHostState() } + val navBarItems = remember(rootComponent.showMap) { getAndroidNavBarItems(rootComponent.showMap) } + + CoffeegramTheme( + themeState = rootComponent.themeState.collectAsState().value, + ) { + Scaffold( + modifier = modifier, + contentWindowInsets = WindowInsets.systemBars, + topBar = { AndroidTopBar(rootComponent) }, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + bottomBar = { AndroidBottomBar(rootComponent, navBarItems) } + ) { paddingValues -> + AndroidCurrentScreen(rootComponent, paddingValues, snackbarHostState) + } + } +} + +@Composable +private fun AndroidTopBar(rootComponent: AndroidRootComponent) { + ChildPages( + pages = rootComponent.pages, + onPageSelected = rootComponent::selectPage, + modifier = Modifier.fillMaxWidth(), + ) { _, page -> + when (val c = page) { + is AndroidRootComponent.Child.CoffeeEdit -> CmpCoffeeEditAppBar(c.component) + is AndroidRootComponent.Child.Stats -> StatsAppBar() + is AndroidRootComponent.Child.Map -> MapAppBar() + is AndroidRootComponent.Child.Settings -> AndroidSettingsAppBar() + } + } +} + +@Composable +private fun AndroidCurrentScreen( + rootComponent: AndroidRootComponent, + paddingValues: PaddingValues, + snackbarHostState: SnackbarHostState, +) { + ChildPages( + pages = rootComponent.pages, + onPageSelected = rootComponent::selectPage, + modifier = Modifier.padding(paddingValues), + ) { _, page -> + when (val c = page) { + is AndroidRootComponent.Child.CoffeeEdit -> CmpCoffeeEditScreen(c.component) + is AndroidRootComponent.Child.Stats -> StatsScreen(c.component) + is AndroidRootComponent.Child.Map -> MapScreen(c.component) + is AndroidRootComponent.Child.Settings -> AndroidSettingsScreen( + c.component, + snackbarHostState = snackbarHostState + ) + } + } +} + +@Composable +private fun AndroidBottomBar( + rootComponent: AndroidRootComponent, + navBarItems: PersistentList, +) { + NavigationBar { + val currentIndex by rootComponent.pages.subscribeAsState() + navBarItems.forEachIndexed { index, item -> + NavigationBarItem( + selected = currentIndex.selectedIndex == index, + onClick = { rootComponent.selectPage(index) }, + label = { Text(stringResource(item.title)) }, + icon = { + Icon( + imageVector = item.icon, + contentDescription = "", + ) + } + ) + } + } +} + +internal fun getAndroidNavBarItems(showMap: Boolean): PersistentList = + if (showMap) { + persistentListOf(calendar, stats, specialty, settings) + } else { + persistentListOf(calendar, stats, settings) + } diff --git a/app/src/main/java/ru/beryukhov/coffeegram/pages/SettingsPage.kt b/app/src/main/java/ru/beryukhov/coffeegram/screens/AndroidSettingsScreen.kt similarity index 66% rename from app/src/main/java/ru/beryukhov/coffeegram/pages/SettingsPage.kt rename to app/src/main/java/ru/beryukhov/coffeegram/screens/AndroidSettingsScreen.kt index a9028cce..9a6a557c 100644 --- a/app/src/main/java/ru/beryukhov/coffeegram/pages/SettingsPage.kt +++ b/app/src/main/java/ru/beryukhov/coffeegram/screens/AndroidSettingsScreen.kt @@ -1,13 +1,12 @@ @file:OptIn(ExperimentalMaterial3Api::class) -package ru.beryukhov.coffeegram.pages +package ru.beryukhov.coffeegram.screens -import android.content.Context import android.os.Build import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.absolutePadding +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Checkbox @@ -15,82 +14,54 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.RadioButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import ru.beryukhov.coffeegram.BuildConfig import ru.beryukhov.coffeegram.R -import ru.beryukhov.coffeegram.app_ui.CoffeegramTheme -import ru.beryukhov.coffeegram.changeIcon +import ru.beryukhov.coffeegram.components.AndroidSettingsComponent import ru.beryukhov.coffeegram.model.DarkThemeState -import ru.beryukhov.coffeegram.model.ThemeIntent -import ru.beryukhov.coffeegram.model.ThemeState -import ru.beryukhov.coffeegram.model.ThemeStore -import ru.beryukhov.coffeegram.repository.ThemeInMemoryStorage -@Preview @Composable -internal fun SettingsPagePreview() { - val snackbarHostState = remember { SnackbarHostState() } - CoffeegramTheme { - Scaffold( - snackbarHost = { - SnackbarHost(hostState = snackbarHostState) - } - ) { - Column(modifier = Modifier.padding(it)) { - SettingsPage( - themeStore = getThemeStoreStub(LocalContext.current), - snackbarHostState = snackbarHostState - ) - } - } - } -} - -@Composable -fun ColumnScope.SettingsPage( - themeStore: ThemeStore, +fun AndroidSettingsScreen( + component: AndroidSettingsComponent, snackbarHostState: SnackbarHostState, modifier: Modifier = Modifier, - startWearableActivity: () -> Unit = {} ) { - Column(modifier = modifier.weight(1f)) { + val themeState by component.models.collectAsState() + + Column(modifier = modifier.fillMaxSize()) { Text( stringResource(R.string.app_theme), style = typography.titleMedium, modifier = Modifier.absolutePadding(left = 24.dp, top = 16.dp) ) - val themeState: ThemeState by themeStore.state.collectAsState() + ThemeRadioButtonWithText( selected = themeState.useDarkTheme == DarkThemeState.SYSTEM, - onClick = { themeStore.newIntent(ThemeIntent.SetSystemIntent) }, + onClick = { component.onSetSystemTheme() }, stringResource(R.string.app_theme_system) ) ThemeRadioButtonWithText( selected = themeState.useDarkTheme == DarkThemeState.LIGHT, - onClick = { themeStore.newIntent(ThemeIntent.SetLightIntent) }, + onClick = { component.onSetLightTheme() }, stringResource(R.string.app_theme_light) ) ThemeRadioButtonWithText( selected = themeState.useDarkTheme == DarkThemeState.DARK, - onClick = { themeStore.newIntent(ThemeIntent.SetDarkIntent) }, + onClick = { component.onSetDarkTheme() }, stringResource(R.string.app_theme_dark) ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val scope = rememberCoroutineScope() ThemeCheckBoxWithText( @@ -104,14 +75,13 @@ fun ColumnScope.SettingsPage( ) } } - themeStore.newIntent(ThemeIntent.SetDynamicIntent(it)) + component.onSetDynamicTheme(it) }, stringResource(R.string.app_theme_dynamic) ) } val scope = rememberCoroutineScope() - val context = LocalContext.current ThemeCheckBoxWithText( checked = themeState.isSummer == true, onCheckedChange = { @@ -123,14 +93,18 @@ fun ColumnScope.SettingsPage( ) } } - changeIcon(context, it) - themeStore.newIntent(ThemeIntent.SetSummerIntent(it)) + component.onSetSummerTheme(it) }, stringResource(R.string.app_theme_summer) ) + HorizontalDivider() + if (BuildConfig.DEBUG) { - Button(onClick = { startWearableActivity() }, modifier = Modifier.padding(16.dp)) { + Button( + onClick = { component.onStartWearableActivity() }, + modifier = Modifier.padding(16.dp) + ) { Text("Start Wearable Activity") } } @@ -138,7 +112,7 @@ fun ColumnScope.SettingsPage( } @Composable -fun SettingsAppBar(modifier: Modifier = Modifier) { +fun AndroidSettingsAppBar(modifier: Modifier = Modifier) { TopAppBar( title = { Text(stringResource(R.string.settings)) }, modifier = modifier @@ -146,7 +120,7 @@ fun SettingsAppBar(modifier: Modifier = Modifier) { } @Composable -fun ThemeRadioButtonWithText( +private fun ThemeRadioButtonWithText( selected: Boolean, onClick: (() -> Unit)?, label: String, @@ -159,7 +133,7 @@ fun ThemeRadioButtonWithText( } @Composable -fun ThemeCheckBoxWithText( +private fun ThemeCheckBoxWithText( checked: Boolean, onCheckedChange: ((Boolean) -> Unit)?, label: String, @@ -170,5 +144,3 @@ fun ThemeCheckBoxWithText( Text(text = label, modifier = Modifier.align(CenterVertically)) } } - -private fun getThemeStoreStub(context: Context) = ThemeStore(ThemeInMemoryStorage()) diff --git a/app/src/main/java/ru/beryukhov/coffeegram/screens/MapScreen.kt b/app/src/main/java/ru/beryukhov/coffeegram/screens/MapScreen.kt new file mode 100644 index 00000000..5e8a68a0 --- /dev/null +++ b/app/src/main/java/ru/beryukhov/coffeegram/screens/MapScreen.kt @@ -0,0 +1,169 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package ru.beryukhov.coffeegram.screens + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.location.LocationManager +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Place +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.google.android.gms.maps.CameraUpdate +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.MapProperties +import com.google.maps.android.compose.MapUiSettings +import com.google.maps.android.compose.MarkerComposable +import com.google.maps.android.compose.MarkerState +import com.google.maps.android.compose.rememberCameraPositionState +import com.google.maps.android.ktx.model.cameraPosition +import ru.beryukhov.coffeegram.R +import ru.beryukhov.coffeegram.components.MapComponent +import ru.beryukhov.coffeegram.repository.latlng +import ru.beryukhov.coffeegram.view.MapMarker + +@Composable +fun MapScreen( + component: MapComponent, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val coarseLocationEnabled = remember { + context.checkSelfPermission( + Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + } + val coarseLocation = remember { + val locationDefault = LatLng(35.1272, 33.3371) + val coarseLocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + try { + coarseLocationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) + ?.let { LatLng(it.latitude, it.longitude) } ?: locationDefault + } catch (_: SecurityException) { + locationDefault + } + } + + if (coarseLocationEnabled) { + val coffeeShopsState by component.coffeeShops.collectAsState() + + val cameraPositionState = rememberCameraPositionState { + position = cameraPosition { + target(coarseLocation) + zoom(10f) + } + } + LaunchedEffect(cameraPositionState.position.zoom) { + component.onZoomChanged(cameraPositionState.position.zoom) + } + Box( + modifier = modifier.fillMaxSize(), + ) { + GoogleMap( + modifier = Modifier.fillMaxSize(), + properties = MapProperties().copy( + isMyLocationEnabled = true + ), + uiSettings = MapUiSettings( + compassEnabled = false, + zoomControlsEnabled = false, + myLocationButtonEnabled = false + ), + cameraPositionState = cameraPositionState + ) { + coffeeShopsState.list.forEach { + MarkerComposable( + keys = arrayOf(it.highlighted, coffeeShopsState.expanded), + state = MarkerState(it.coffeeShop.latlng()), + onClick = { _ -> + component.onMarkerClicked(it.coffeeShop) + true + }, + zIndex = if (it.highlighted) 1f else 0f + ) { + MapMarker( + name = it.coffeeShop.name, + descr = it.coffeeShop.description, + highlighted = it.highlighted, + expanded = coffeeShopsState.expanded, + ) + } + } + } + + val density = LocalDensity.current.density + Button( + onClick = { + panMapToFitAllMarkers(coffeeShopsState.list.map { it.coffeeShop.latlng() }, density)?.let { + cameraPositionState.move(it) + } + }, + modifier = Modifier.padding(16.dp) + ) { + Icon( + imageVector = Icons.Rounded.Place, + contentDescription = "" + ) + } + } + } else { + Box( + modifier = modifier.fillMaxSize(), + ) { + Text( + text = stringResource(R.string.location_permission_required), + modifier = Modifier.align(Alignment.Center) + ) + } + } +} + +private fun panMapToFitAllMarkers(locations: List, density: Float): CameraUpdate? = + when { + locations.isEmpty() -> { + null + } + locations.size == 1 -> { + CameraUpdateFactory.newCameraPosition(cameraPosition { + target(locations.first()) + }) + } + else -> { + val latLng = LatLngBounds.builder().apply { + locations.forEach { include(it) } + }.build() + CameraUpdateFactory.newLatLngBounds( + latLng, + (72 * density).toInt() + ) + } + } + +@Composable +fun MapAppBar(modifier: Modifier = Modifier) { + TopAppBar( + title = { Text(stringResource(R.string.map_long)) }, + modifier = modifier + ) +} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/screens/StatsScreen.kt b/app/src/main/java/ru/beryukhov/coffeegram/screens/StatsScreen.kt new file mode 100644 index 00000000..7012c6b2 --- /dev/null +++ b/app/src/main/java/ru/beryukhov/coffeegram/screens/StatsScreen.kt @@ -0,0 +1,37 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package ru.beryukhov.coffeegram.screens + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import ru.beryukhov.coffeegram.R +import ru.beryukhov.coffeegram.components.StatsComponent +import ru.beryukhov.coffeegram.pages.CoffeeCharts + +@Composable +fun StatsScreen( + component: StatsComponent, + modifier: Modifier = Modifier, +) { + val state by component.models.collectAsState() + + Column(modifier = modifier.fillMaxSize()) { + CoffeeCharts(coffeeState = state, modifier = Modifier.weight(1f)) + } +} + +@Composable +fun StatsAppBar(modifier: Modifier = Modifier) { + TopAppBar( + title = { Text(stringResource(R.string.stats)) }, + modifier = modifier + ) +} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/view/MapMarker.kt b/app/src/main/java/ru/beryukhov/coffeegram/view/MapMarker.kt new file mode 100644 index 00000000..7cfc7ce6 --- /dev/null +++ b/app/src/main/java/ru/beryukhov/coffeegram/view/MapMarker.kt @@ -0,0 +1,115 @@ +package ru.beryukhov.coffeegram.view + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import ru.beryukhov.coffeegram.R +import ru.beryukhov.coffeegram.pages.boxShadow + +@Composable +@Preview +private fun SmallMarker() = MapMarker(expanded = false) + +@Composable +@Preview +private fun ExpandedMarker() = MapMarker(expanded = true) + +@Composable +fun MapMarker( + modifier: Modifier = Modifier, + name: String = "Title", + descr: String = "Subtitle", + highlighted: Boolean = false, + expanded: Boolean = false, +) { + val borderRadius = if (expanded) 12.dp else 6.dp + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .padding(16.dp) + .boxShadow( + blurRadius = 3.dp, + offset = DpOffset(x = 0.dp, y = 2.dp), + shape = RoundedCornerShape(borderRadius), + color = Color(0f, 0f, 0f, 0.15f) + ) + .boxShadow( + blurRadius = 9.dp, + offset = DpOffset(x = 0.dp, y = 6.dp), + shape = RoundedCornerShape(borderRadius), + color = Color(0f, 0f, 0f, 0.04f) + ) + .background( + color = if (highlighted) Color(0xFFE8E5E3) else Color(0xFFFFFFFF), + shape = RoundedCornerShape(size = 6.dp) + ) + .padding(start = 4.dp, top = 3.dp, end = 8.dp, bottom = 3.dp) + .widthIn(min = 0.dp, max = (LocalConfiguration.current.screenWidthDp / if (highlighted) 1 else 2).dp) + + ) { + Image( + painter = painterResource(id = R.drawable.logo_splash), + contentDescription = "image description", + contentScale = ContentScale.Fit, + modifier = Modifier + .padding(start = 0.dp, top = 1.dp, end = 2.dp, bottom = 1.dp) + .width(18.dp) + .height(18.dp) + + ) + Column( + verticalArrangement = Arrangement.spacedBy(if (highlighted) 4.dp else -4.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.Start, + modifier = Modifier.padding(start = 4.dp) + ) { + val textColor = MaterialTheme.colorScheme.onPrimaryContainer + Text( + text = name, + style = TextStyle( + fontSize = 17.sp, + lineHeight = 24.sp, + fontWeight = FontWeight(350), + color = textColor, + ), + maxLines = if (highlighted) 2 else 1, + overflow = TextOverflow.Ellipsis + ) + if (expanded) { + Text( + text = descr, + style = TextStyle( + fontSize = 13.sp, + lineHeight = 18.sp, + fontWeight = FontWeight(350), + color = textColor, + ), + maxLines = if (highlighted) 3 else 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } +} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/wearable/WearableSyncService.kt b/app/src/main/java/ru/beryukhov/coffeegram/wearable/WearableSyncService.kt new file mode 100644 index 00000000..54b479df --- /dev/null +++ b/app/src/main/java/ru/beryukhov/coffeegram/wearable/WearableSyncService.kt @@ -0,0 +1,77 @@ +package ru.beryukhov.coffeegram.wearable + +import android.content.Context +import android.util.Log +import com.google.android.gms.wearable.PutDataMapRequest +import com.google.android.gms.wearable.Wearable +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await +import ru.beryukhov.coffeegram.data.DAY_COFFEE_PATH +import ru.beryukhov.coffeegram.data.DayCoffee +import ru.beryukhov.coffeegram.data.START_ACTIVITY_PATH +import ru.beryukhov.coffeegram.data.toDataMap + +private const val TAG = "WearableSyncService" + +/** + * Service for syncing data with wearable devices. + */ +class WearableSyncService( + private val context: Context, +) { + private val nodeClient by lazy { Wearable.getNodeClient(context) } + private val messageClient by lazy { Wearable.getMessageClient(context) } + private val dataClient by lazy { Wearable.getDataClient(context) } + + /** + * Start the wearable activity on connected devices. + */ + fun startWearableActivity(scope: CoroutineScope, dayCoffee: DayCoffee? = null) { + scope.launch { + try { + val nodes = nodeClient.connectedNodes.await() + + // Send a message to all nodes in parallel + nodes.map { node -> + async { + messageClient.sendMessage(node.id, START_ACTIVITY_PATH, byteArrayOf()).await() + } + }.awaitAll() + + Log.d(TAG, "Starting activity requests sent successfully") + } catch (cancellationException: CancellationException) { + throw cancellationException + } catch (exception: Exception) { + Log.d(TAG, "Starting activity failed: $exception") + } + } + + // Also send current day coffee data if provided + dayCoffee?.let { sendDayCoffee(scope, it) } + } + + /** + * Send the current day coffee data to wearable devices. + */ + fun sendDayCoffee(scope: CoroutineScope, dayCoffee: DayCoffee) { + scope.launch { + try { + val request = PutDataMapRequest.create(DAY_COFFEE_PATH).apply { + dayCoffee.toDataMap(dataMap) + }.asPutDataRequest().setUrgent() + + val result = dataClient.putDataItem(request).await() + + Log.d(TAG, "DataItem saved: $result") + } catch (cancellationException: CancellationException) { + throw cancellationException + } catch (exception: Exception) { + Log.d(TAG, "Saving DataItem failed: $exception") + } + } + } +} diff --git a/app/src/main/java/ru/beryukhov/coffeegram/widget/FirstGlanceWidget.kt b/app/src/main/java/ru/beryukhov/coffeegram/widget/FirstGlanceWidget.kt index 032b8d27..c3c559fa 100644 --- a/app/src/main/java/ru/beryukhov/coffeegram/widget/FirstGlanceWidget.kt +++ b/app/src/main/java/ru/beryukhov/coffeegram/widget/FirstGlanceWidget.kt @@ -1,7 +1,11 @@ package ru.beryukhov.coffeegram.widget import android.content.Context +import android.content.res.Configuration import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Canvas @@ -11,6 +15,8 @@ import androidx.compose.ui.graphics.ImageBitmapConfig import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.graphics.drawscope.CanvasDrawScope import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.LayoutDirection @@ -23,6 +29,7 @@ import androidx.glance.GlanceModifier import androidx.glance.GlanceTheme import androidx.glance.Image import androidx.glance.ImageProvider +import androidx.glance.LocalContext import androidx.glance.LocalSize import androidx.glance.action.ActionParameters import androidx.glance.action.action @@ -54,18 +61,19 @@ import androidx.glance.text.TextAlign import androidx.glance.text.TextStyle import androidx.glance.unit.ColorProvider import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList +import org.jetbrains.compose.resources.PreviewContextConfigurationEffect import org.koin.core.component.KoinComponent import org.koin.core.component.inject import ru.beryukhov.coffeegram.MainActivity import ru.beryukhov.coffeegram.R import ru.beryukhov.coffeegram.data.CoffeeType import ru.beryukhov.coffeegram.data.CoffeeTypeWithCount +import ru.beryukhov.coffeegram.data.CoffeeTypes import ru.beryukhov.coffeegram.data.printableText -import ru.beryukhov.coffeegram.model.NavigationState.Companion.NAVIGATION_STATE_KEY -import ru.beryukhov.coffeegram.model.NavigationState.Companion.TODAYS_COFFEE_LIST -import ru.beryukhov.coffeegram.pages.AppWidgetViewModel -import ru.beryukhov.coffeegram.pages.AppWidgetViewModelImpl -import ru.beryukhov.coffeegram.pages.AppWidgetViewModelStub +import ru.beryukhov.coffeegram.model.NavigationConstants.NAVIGATION_STATE_KEY +import ru.beryukhov.coffeegram.model.NavigationConstants.TODAYS_COFFEE_LIST +import ru.beryukhov.coffeegram.pages.WidgetDataBridgeStub import ru.beryukhov.coffeegram.widget.FirstGlanceWidget.Companion.BIG_SQUARE import ru.beryukhov.coffeegram.widget.FirstGlanceWidget.Companion.HORIZONTAL_RECTANGLE import kotlin.math.roundToInt @@ -79,10 +87,10 @@ class FirstGlanceWidget : GlanceAppWidget(errorUiLayout = R.layout.layout_widget SizeMode.Responsive(setOf(SMALL_SQUARE, HORIZONTAL_RECTANGLE, BIG_SQUARE)) override suspend fun provideGlance(context: Context, id: GlanceId) { - val viewModel: AppWidgetViewModelImpl by inject() + val viewModel: WidgetDataBridge by inject() provideContent { // todo widgets are broken because of compose resources - // WidgetContent(viewModel) + WidgetContent(viewModel) } } @@ -99,34 +107,56 @@ class FirstGlanceWidget : GlanceAppWidget(errorUiLayout = R.layout.layout_widget @Preview(widthDp = 50, heightDp = 50) @Preview(widthDp = 200, heightDp = 100) @Preview(widthDp = 300, heightDp = 300) +@Composable +private fun WidgetContentPreview() { + PreviewContextConfigurationEffect() + WidgetContent(WidgetDataBridgeStub) +} + @Composable internal fun WidgetContent( - viewModel: AppWidgetViewModel = AppWidgetViewModelStub(), + dataBridge: WidgetDataBridge, ) { val size = LocalSize.current - GlanceTheme { - Scaffold( - backgroundColor = GlanceTheme.colors.widgetBackground, - horizontalPadding = 0.dp, - ) { - when { - size.width < HORIZONTAL_RECTANGLE.width -> - SmallWidget( - count = viewModel.getCurrentDayCupsCount() - ) + CompositionLocalProvider( + LocalConfiguration provides Configuration(), + LocalDensity provides Density(LocalContext.current) + ) { + GlanceTheme { + Scaffold( + backgroundColor = GlanceTheme.colors.widgetBackground, + horizontalPadding = 0.dp, + ) { + when { + size.width < HORIZONTAL_RECTANGLE.width -> { + val count by dataBridge.getCurrentDayCupsCount().collectAsState(0) + SmallWidget( + count = count + ) + } - size.height < BIG_SQUARE.height -> - HorizontalWidget( - coffeeTypeWithCount = viewModel.getCurrentDayMostPopularWithCount(), - increment = viewModel::currentDayIncrement, - decrement = viewModel::currentDayDecrement - ) + size.height < BIG_SQUARE.height -> { + val data by dataBridge.getCurrentDayMostPopularWithCount().collectAsState( + CoffeeTypeWithCount(CoffeeTypes.Espresso, 0) + ) + HorizontalWidget( + coffeeTypeWithCount = data, + increment = dataBridge::incrementCoffee, + decrement = dataBridge::decrementCoffee + ) + } - else -> BigWidget( - list = viewModel.getCurrentDayList(), - increment = viewModel::currentDayIncrement, - decrement = viewModel::currentDayDecrement - ) + else -> { + val data by dataBridge.getCurrentDayList().collectAsState( + emptyList().toPersistentList() + ) + BigWidget( + list = data, + increment = dataBridge::incrementCoffee, + decrement = dataBridge::decrementCoffee + ) + } + } } } } diff --git a/app/src/main/java/ru/beryukhov/coffeegram/widget/WidgetDataBridge.kt b/app/src/main/java/ru/beryukhov/coffeegram/widget/WidgetDataBridge.kt new file mode 100644 index 00000000..c26b705b --- /dev/null +++ b/app/src/main/java/ru/beryukhov/coffeegram/widget/WidgetDataBridge.kt @@ -0,0 +1,74 @@ +package ru.beryukhov.coffeegram.widget + +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.datetime.TimeZone +import kotlinx.datetime.todayIn +import ru.beryukhov.coffeegram.data.CoffeeType +import ru.beryukhov.coffeegram.data.CoffeeTypeWithCount +import ru.beryukhov.coffeegram.data.CoffeeTypes +import ru.beryukhov.coffeegram.data.DayCoffee +import ru.beryukhov.coffeegram.data.withEmpty +import ru.beryukhov.coffeegram.model.DaysCoffeesIntent +import ru.beryukhov.coffeegram.model.DaysCoffeesStore +import kotlin.time.Clock +import kotlin.time.ExperimentalTime + +/** + * Bridge between Glance widget and Store pattern. + * Provides widget-specific data access to the DaysCoffeesStore. + */ +interface WidgetDataBridge { + fun getCurrentDayCupsCount(): Flow + fun getCurrentDayMostPopularWithCount(): Flow + fun getCurrentDayList(): Flow> + fun incrementCoffee(coffeeType: CoffeeType) + fun decrementCoffee(coffeeType: CoffeeType) +} + +class DefaultWidgetDataBridge( + private val daysCoffeesStore: DaysCoffeesStore, +) : WidgetDataBridge { + + @OptIn(ExperimentalTime::class) + private fun getCurrentDay() = Clock.System.todayIn(TimeZone.currentSystemDefault()) + + override fun getCurrentDayCupsCount(): Flow = + getCurrentDayList().map { it.sumOf { it.count } } + + override fun getCurrentDayMostPopularWithCount(): Flow = getCurrentDayList().map { list -> + if (list.isEmpty()) { + CoffeeTypeWithCount(CoffeeTypes.Cappuccino, 0) + } else { + list.first() + } + } + + override fun getCurrentDayList(): Flow> = + daysCoffeesStore.state.map { dayCoffeeState -> + val dayCoffee = dayCoffeeState.coffees[getCurrentDay()] ?: DayCoffee() + val list = dayCoffee.coffeeCountMap.withEmpty().toList() + .sortedByDescending { it.count } + list.toPersistentList() + } + + override fun incrementCoffee(coffeeType: CoffeeType) { + daysCoffeesStore.newIntent( + DaysCoffeesIntent.PlusCoffee( + localDate = getCurrentDay(), + coffeeType = coffeeType + ) + ) + } + + override fun decrementCoffee(coffeeType: CoffeeType) { + daysCoffeesStore.newIntent( + DaysCoffeesIntent.MinusCoffee( + localDate = getCurrentDay(), + coffeeType = coffeeType + ) + ) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5c2a71f..9c563b96 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,4 +17,5 @@ Loading widget All Weekly + You should give location permission diff --git a/app/src/test/kotlin/ru/beryukhov/coffeegram/TestApplication.kt b/app/src/test/kotlin/ru/beryukhov/coffeegram/TestApplication.kt new file mode 100644 index 00000000..422e8839 --- /dev/null +++ b/app/src/test/kotlin/ru/beryukhov/coffeegram/TestApplication.kt @@ -0,0 +1,5 @@ +package ru.beryukhov.coffeegram + +class TestApplication : Application() { + override suspend fun setWidgetPreview() = Unit +} diff --git a/app/src/test/kotlin/ru/beryukhov/coffeegram/ui_test/ComposeAppTest.kt b/app/src/test/kotlin/ru/beryukhov/coffeegram/ui_test/ComposeAppTest.kt index 00f9f8e1..c237a379 100644 --- a/app/src/test/kotlin/ru/beryukhov/coffeegram/ui_test/ComposeAppTest.kt +++ b/app/src/test/kotlin/ru/beryukhov/coffeegram/ui_test/ComposeAppTest.kt @@ -1,7 +1,5 @@ package ru.beryukhov.coffeegram.ui_test -import android.content.ContentProvider -import android.util.Log import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createAndroidComposeRule @@ -17,7 +15,6 @@ import org.junit.runner.RunWith import org.koin.core.context.loadKoinModules import org.koin.core.context.stopKoin import org.koin.dsl.module -import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import repository.CoffeeRepository import repository.InMemoryCoffeeRepository @@ -25,12 +22,11 @@ import ru.beryukhov.coffeegram.MainActivity @OptIn(ExperimentalTestApi::class) @RunWith(RobolectricTestRunner::class) -@Ignore("todo fix - test hanging") +@Ignore("todo fix") class ComposeAppTest { @get:Rule val composeTestRule by lazy { replaceRoomWithInMemoryStorage() - setupAndroidContextProvider() createAndroidComposeRule() } @@ -49,26 +45,6 @@ class ComposeAppTest { stopKoin() } - // Configures Compose's AndroidContextProvider to access resources in tests. - // See https://youtrack.jetbrains.com/issue/CMP-6612 - private fun setupAndroidContextProvider() { - val type = findAndroidContextProvider() ?: return - Robolectric.setupContentProvider(type) - } - - private fun findAndroidContextProvider(): Class? { - val providerClassName = "org.jetbrains.compose.resources.AndroidContextProvider" - return try { - @Suppress("UNCHECKED_CAST") - Class.forName(providerClassName) as Class - } catch (_: ClassNotFoundException) { - Log.d("Robolectric", "Class not found: $providerClassName") - // Tests that don't depend on Compose will not have the provider class in classpath and will get - // ClassNotFoundException. Skip configuring the provider for them. - null - } - } - @Test fun testDayOpen() { onComposeScreen { diff --git a/app/src/test/kotlin/ru/beryukhov/coffeegram/ui_test/ComposeScreenTest.kt b/app/src/test/kotlin/ru/beryukhov/coffeegram/ui_test/ComposeScreenTest.kt index d85e2fe8..702f6b87 100644 --- a/app/src/test/kotlin/ru/beryukhov/coffeegram/ui_test/ComposeScreenTest.kt +++ b/app/src/test/kotlin/ru/beryukhov/coffeegram/ui_test/ComposeScreenTest.kt @@ -2,24 +2,31 @@ package ru.beryukhov.coffeegram.ui_test import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.junit4.createComposeRule +import com.arkivanov.decompose.DefaultComponentContext +import com.arkivanov.essenty.lifecycle.LifecycleRegistry import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.rule.KakaoComposeTestRule -import kotlinx.datetime.Month +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import org.junit.After -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.koin.core.context.stopKoin import org.robolectric.RobolectricTestRunner -import ru.beryukhov.coffeegram.PagesContent +import org.robolectric.annotation.Config import ru.beryukhov.coffeegram.PreviewContextConfigurationEffectProvider -import ru.beryukhov.coffeegram.model.NavigationStore -import ru.beryukhov.date_time_utils.YearMonth -import ru.beryukhov.date_time_utils.nowYM +import ru.beryukhov.coffeegram.TestApplication +import ru.beryukhov.coffeegram.components.DefaultAndroidRootComponent +import ru.beryukhov.coffeegram.model.DaysCoffeesIntent +import ru.beryukhov.coffeegram.model.DaysCoffeesState +import ru.beryukhov.coffeegram.model.DaysCoffeesStore +import ru.beryukhov.coffeegram.model.ThemeStore +import ru.beryukhov.coffeegram.repository.ThemeInMemoryStorage +import ru.beryukhov.coffeegram.screens.AndroidRootScreen @RunWith(RobolectricTestRunner::class) -@Ignore("todo fix koin tests initialization") +@Config(application = TestApplication::class) class ComposeScreenTest { @get:Rule val composeTestRule by lazy { @@ -29,6 +36,7 @@ class ComposeScreenTest { @get:Rule val kakaoComposeTestRule = KakaoComposeTestRule(composeTestRule) + @After fun tearDown() { stopKoin() @@ -36,47 +44,45 @@ class ComposeScreenTest { @Test fun testYear() { - withRule(yearMonth = YearMonth(2020, Month(1))) { + withRule { onComposeScreen { -// yearName.assertIsDisplayed() // todo false negative for some reason monthName.assertIsDisplayed() - yearName.assertTextEquals("2020") - monthName.assertTextEquals("January") } } } @Test fun testMonthChange() { - withRule(yearMonth = YearMonth(2020, Month(9))) { + withRule { onComposeScreen { - monthName.assertTextEquals("September") - leftArrowButton { - assertIsDisplayed() - performClick() - } - monthName.assertTextEquals("August") - rightArrowButton { - assertIsDisplayed() - performClick() - } - monthName.assertTextEquals("September") - rightArrowButton { - assertIsDisplayed() - performClick() - } - monthName.assertTextEquals("October") + monthName.assertIsDisplayed() } } } - private inline fun withRule(yearMonth: YearMonth = nowYM(), block: ComposeTestRule.() -> R): R = + private inline fun withRule(block: ComposeTestRule.() -> R): R = with(composeTestRule) { setContent { PreviewContextConfigurationEffectProvider() - PagesContent( - navigationStore = NavigationStore(yearMonth = yearMonth), + + val lifecycle = LifecycleRegistry() + val componentContext = DefaultComponentContext(lifecycle) + val themeStore = ThemeStore(ThemeInMemoryStorage()) + val daysCoffeesStore = object : DaysCoffeesStore { + override val state: StateFlow = MutableStateFlow(DaysCoffeesState()) + override fun newIntent(intent: DaysCoffeesIntent) = Unit + } + + val rootComponent = DefaultAndroidRootComponent( + context = componentContext, + themeStore = themeStore, + daysCoffeesStore = daysCoffeesStore, + showMap = false, + onStartWearableActivity = {}, + onIconChange = {}, ) + + AndroidRootScreen(rootComponent = rootComponent) } block() } diff --git a/cmp-app/.gitignore b/cmp-app/.gitignore deleted file mode 100644 index 42afabfd..00000000 --- a/cmp-app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/cmp-app/build.gradle.kts b/cmp-app/build.gradle.kts deleted file mode 100644 index b35d8a3d..00000000 --- a/cmp-app/build.gradle.kts +++ /dev/null @@ -1,51 +0,0 @@ -plugins { - id("com.android.application") - id("com.autonomousapps.dependency-analysis") - kotlin("android") - id("org.jetbrains.compose") - id("org.jetbrains.kotlin.plugin.compose") -} - -android { - compileSdk = libs.versions.compileSdk.get().toInt() - - defaultConfig { - applicationId = "ru.beryukhov.coffeegram.desktop" - minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = 1 - versionName = "1.0" - } - - buildTypes { - getByName("release") { - isMinifyEnabled = false - proguardFile(getDefaultProguardFile("proguard-android-optimize.txt")) - proguardFile("proguard-rules.pro") - } - } - compileOptions { - targetCompatibility = JavaVersion.VERSION_21 - sourceCompatibility = JavaVersion.VERSION_21 - } - namespace = "ru.beryukhov.coffeegram" -} - -kotlin { - jvmToolchain(21) -} - -dependencies { - - implementation(projects.cmpCommon) - - implementation(libs.cmp.runtime) - implementation(libs.cmp.foundation) - - implementation(libs.material) - - implementation(libs.compose.activity) - runtimeOnly(libs.coroutines.android) - - implementation(libs.koin.android) -} diff --git a/cmp-app/proguard-rules.pro b/cmp-app/proguard-rules.pro deleted file mode 100644 index 481bb434..00000000 --- a/cmp-app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/cmp-app/src/main/AndroidManifest.xml b/cmp-app/src/main/AndroidManifest.xml deleted file mode 100644 index 8e82d2e5..00000000 --- a/cmp-app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/cmp-app/src/main/java/ru/beryukhov/coffeegram/Application.kt b/cmp-app/src/main/java/ru/beryukhov/coffeegram/Application.kt deleted file mode 100644 index e44dd810..00000000 --- a/cmp-app/src/main/java/ru/beryukhov/coffeegram/Application.kt +++ /dev/null @@ -1,21 +0,0 @@ -package ru.beryukhov.coffeegram - -import android.app.Application -import org.koin.android.ext.koin.androidContext -import org.koin.android.ext.koin.androidLogger -import org.koin.core.context.GlobalContext.startKoin -import org.koin.dsl.module - -@Suppress("unused") -class Application : Application() { - private val androidModule = module {} - - override fun onCreate() { - super.onCreate() - startKoin { - androidContext(this@Application) - androidLogger() - modules(appModule + androidModule) - } - } -} diff --git a/cmp-app/src/main/java/ru/beryukhov/coffeegram/MainActivity.kt b/cmp-app/src/main/java/ru/beryukhov/coffeegram/MainActivity.kt deleted file mode 100644 index 36a455e1..00000000 --- a/cmp-app/src/main/java/ru/beryukhov/coffeegram/MainActivity.kt +++ /dev/null @@ -1,38 +0,0 @@ -package ru.beryukhov.coffeegram - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Box -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import com.arkivanov.decompose.defaultComponentContext -import org.koin.android.ext.android.get -import ru.beryukhov.coffeegram.animations.newSplashTransition -import ru.beryukhov.coffeegram.components.DefaultRootComponent -import ru.beryukhov.coffeegram.pages.LandingPage -import ru.beryukhov.coffeegram.screens.RootScreen - -class MainActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val rootComponent = DefaultRootComponent( - defaultComponentContext(), - themeStore = get(), - daysCoffeesStore = get(), - ) - setContent { - val transition = newSplashTransition() - Box { - LandingPage( - modifier = Modifier.alpha(transition.splashAlpha), - ) - RootScreen( - rootComponent, - modifier = Modifier.alpha(transition.contentAlpha), - ) - } - } - } -} diff --git a/cmp-app/src/main/java/ru/beryukhov/coffeegram/animations/SplashTransition.kt b/cmp-app/src/main/java/ru/beryukhov/coffeegram/animations/SplashTransition.kt deleted file mode 100644 index 90fb04be..00000000 --- a/cmp-app/src/main/java/ru/beryukhov/coffeegram/animations/SplashTransition.kt +++ /dev/null @@ -1,54 +0,0 @@ -package ru.beryukhov.coffeegram.animations - -import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.animateDp -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.rememberTransition -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -enum class SplashState { Shown, Completed } - -data class SplashTransition( - val splashAlpha: Float, - val contentAlpha: Float, - val contentTopPadding: Dp -) - -@Composable -fun newSplashTransition(): SplashTransition { - val visibleState = remember { MutableTransitionState(SplashState.Shown) } - visibleState.targetState = SplashState.Completed - val transition = rememberTransition(visibleState) - val splashAlpha by transition.animateFloat( - transitionSpec = { tween(1000) } - ) { splashState -> - when (splashState) { - SplashState.Shown -> 1f - SplashState.Completed -> 0f - } - } - val contentAlpha by transition.animateFloat( - transitionSpec = { tween(3000) } - ) { splashState -> - when (splashState) { - SplashState.Shown -> 0f - SplashState.Completed -> 1f - } - } - val contentTopPadding by transition.animateDp( - transitionSpec = { spring(stiffness = Spring.StiffnessLow) } - ) { splashState -> - when (splashState) { - SplashState.Shown -> 100.dp - SplashState.Completed -> 0.dp - } - } - return SplashTransition(splashAlpha, contentAlpha, contentTopPadding) -} diff --git a/cmp-app/src/main/java/ru/beryukhov/coffeegram/pages/LandingScreen.kt b/cmp-app/src/main/java/ru/beryukhov/coffeegram/pages/LandingScreen.kt deleted file mode 100644 index f3439dfd..00000000 --- a/cmp-app/src/main/java/ru/beryukhov/coffeegram/pages/LandingScreen.kt +++ /dev/null @@ -1,22 +0,0 @@ -package ru.beryukhov.coffeegram.pages - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import ru.beryukhov.coffeegram.R - -@Composable -fun LandingPage(modifier: Modifier = Modifier) { - Box( - modifier = modifier.fillMaxSize().background(Color(0xff795548)), - contentAlignment = Alignment.Center - ) { - Image(painter = painterResource(id = R.drawable.logo_splash), contentDescription = "") - } -} diff --git a/cmp-app/src/main/res/drawable/background_splash.xml b/cmp-app/src/main/res/drawable/background_splash.xml deleted file mode 100644 index 09f3dcfd..00000000 --- a/cmp-app/src/main/res/drawable/background_splash.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/cmp-app/src/main/res/drawable/ic_launcher_background.xml b/cmp-app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9c..00000000 --- a/cmp-app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/cmp-app/src/main/res/drawable/ic_launcher_foreground.xml b/cmp-app/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 54eef3ec..00000000 --- a/cmp-app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - diff --git a/cmp-app/src/main/res/drawable/logo_splash.png b/cmp-app/src/main/res/drawable/logo_splash.png deleted file mode 100644 index a67243fd..00000000 Binary files a/cmp-app/src/main/res/drawable/logo_splash.png and /dev/null differ diff --git a/cmp-app/src/main/res/mipmap-anydpi/ic_launcher.xml b/cmp-app/src/main/res/mipmap-anydpi/ic_launcher.xml deleted file mode 100644 index ebcd4199..00000000 --- a/cmp-app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/cmp-app/src/main/res/values-night/themes.xml b/cmp-app/src/main/res/values-night/themes.xml deleted file mode 100644 index 7c4e9542..00000000 --- a/cmp-app/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/cmp-app/src/main/res/values/colors.xml b/cmp-app/src/main/res/values/colors.xml deleted file mode 100644 index bd56e489..00000000 --- a/cmp-app/src/main/res/values/colors.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - #FFBCAAA4 - #FF795548 - #FF5D4037 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - diff --git a/cmp-app/src/main/res/values/strings.xml b/cmp-app/src/main/res/values/strings.xml deleted file mode 100644 index c3c1a620..00000000 --- a/cmp-app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - Coffeegram - \ No newline at end of file diff --git a/cmp-app/src/main/res/values/themes.xml b/cmp-app/src/main/res/values/themes.xml deleted file mode 100644 index 5d0730aa..00000000 --- a/cmp-app/src/main/res/values/themes.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - diff --git a/cmp-common/build.gradle.kts b/cmp-common/build.gradle.kts index fbf5451a..1a5990e9 100644 --- a/cmp-common/build.gradle.kts +++ b/cmp-common/build.gradle.kts @@ -11,7 +11,6 @@ plugins { id("org.jetbrains.kotlin.plugin.compose") id("org.jetbrains.kotlin.plugin.serialization") id("org.jetbrains.compose.hot-reload") - `maven-publish` } diff --git a/cmp-common/src/commonMain/kotlin/ru/beryukhov/coffeegram/model/DaysCoffeesStore.kt b/cmp-common/src/commonMain/kotlin/ru/beryukhov/coffeegram/model/DaysCoffeesStore.kt index 6c49ead6..5debf316 100644 --- a/cmp-common/src/commonMain/kotlin/ru/beryukhov/coffeegram/model/DaysCoffeesStore.kt +++ b/cmp-common/src/commonMain/kotlin/ru/beryukhov/coffeegram/model/DaysCoffeesStore.kt @@ -9,8 +9,7 @@ import ru.beryukhov.coffeegram.store_lib.StoreImpl interface DaysCoffeesStore : Store -// todo make internal after apps merge will be finished -class DaysCoffeesStoreImpl(coffeeStorage: CoffeeStorage) : DaysCoffeesStore, +internal class DaysCoffeesStoreImpl(coffeeStorage: CoffeeStorage) : DaysCoffeesStore, StoreImpl( initialState = DaysCoffeesState(), storage = coffeeStorage diff --git a/cmp-common/src/commonMain/kotlin/ru/beryukhov/coffeegram/screens/MonthTableScreen.kt b/cmp-common/src/commonMain/kotlin/ru/beryukhov/coffeegram/screens/MonthTableScreen.kt index 06e80c2e..a19b73a7 100644 --- a/cmp-common/src/commonMain/kotlin/ru/beryukhov/coffeegram/screens/MonthTableScreen.kt +++ b/cmp-common/src/commonMain/kotlin/ru/beryukhov/coffeegram/screens/MonthTableScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag @@ -67,7 +68,7 @@ fun MonthTableAppBar( title = { Row(horizontalArrangement = Arrangement.Center) { Text( - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f).testTag("Month"), text = AnnotatedString( text = getFullMonthName(screenState.yearMonth.month), paragraphStyle = ParagraphStyle(textAlign = TextAlign.Center) diff --git a/settings.gradle.kts b/settings.gradle.kts index d1437f91..71aacebd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,7 +7,6 @@ include(":repository-room") includeBuild("build-logic") include(":wear") -include("cmp-app") include("cmp-common") include("repository") include("repository-sqldelight")