From fecc6598b8a13240dd4b9268c96fb5c641ab41fd Mon Sep 17 00:00:00 2001 From: BoD Date: Sun, 24 Mar 2024 00:50:36 +0100 Subject: [PATCH 1/7] Show session list (day 1, day 2) --- gradle/libs.versions.toml | 4 +- .../graphql/ApolloClientBuilder.android.kt | 6 +- .../store/firebase/FirebaseUserRepository.kt | 2 +- .../domain/repo/UserRepository.kt | 2 +- wearApp/build.gradle.kts | 15 +-- wearApp/google-services.json | 56 +-------- wearApp/keystore.debug | 1 + wearApp/keystore.release | 1 + .../wear/AndroidMakersWearApplication.kt | 4 + .../androidmakers/wear/di/ViewModelModule.kt | 4 +- .../wear/ui/main/MainActivity.kt | 118 ++++++++++++------ .../wear/ui/main/MainViewModel.kt | 61 ++++++++- .../androidmakers/wear/ui/main/Navigation.kt | 6 + .../wear/ui/main/SessionDetails.kt | 11 ++ .../wear/ui/session/list/SessionListScreen.kt | 66 ++++++++++ .../wear/ui/signin/GoogleSignInViewModel.kt | 43 +++++++ .../wear/ui/signin/SignInScreen.kt | 41 ++++++ .../wear/ui/signin/SignInViewModel.kt | 26 ++++ wearApp/src/main/res/values-fr/strings.xml | 4 + wearApp/src/main/res/values/strings.xml | 5 +- 20 files changed, 359 insertions(+), 117 deletions(-) mode change 100644 => 120000 wearApp/google-services.json create mode 120000 wearApp/keystore.debug create mode 120000 wearApp/keystore.release create mode 100644 wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/Navigation.kt create mode 100644 wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/SessionDetails.kt create mode 100644 wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt create mode 100644 wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/GoogleSignInViewModel.kt create mode 100644 wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInScreen.kt create mode 100644 wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInViewModel.kt create mode 100644 wearApp/src/main/res/values-fr/strings.xml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9317c2eb..c2243064 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,7 @@ pullrefresh = "1.3.0" androidx-wear-compose = "1.3.0" playServicesWearable = "18.1.0" compose-ui-tooling = "1.3.0" -horologist = "0.5.24" +horologist = "0.6.5" [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } @@ -45,6 +45,7 @@ compose-material = { group = "androidx.compose.material", name = "material", ver compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } espresso-core = "androidx.test.espresso:espresso-core:3.5.1" firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx" } +firebase-auth-ktx = { module = "com.google.firebase:firebase-auth-ktx" } firebase-bom = "com.google.firebase:firebase-bom:32.7.2" firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx" } firebase-inappmessaging = { module = "com.google.firebase:firebase-inappmessaging-display-ktx" } @@ -95,6 +96,7 @@ androidx-material-icons-core = { module = "androidx.compose.material:material-ic horologist-composables = { module = "com.google.android.horologist:horologist-composables", version.ref = "horologist" } horologist-compose-layout = { module = "com.google.android.horologist:horologist-compose-layout", version.ref = "horologist" } horologist-compose-material = { module = "com.google.android.horologist:horologist-compose-material", version.ref = "horologist" } +horologist-auth-ui = { module = "com.google.android.horologist:horologist-auth-ui", version.ref = "horologist" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } androidx-compose-ui-tooling = { module = "androidx.wear.compose:compose-ui-tooling", version.ref = "compose-ui-tooling" } wear-compose-navigation = { module = "androidx.wear.compose:compose-navigation", version.ref = "androidx-wear-compose" } diff --git a/shared/data/src/androidMain/kotlin/fr/androidmakers/store/graphql/ApolloClientBuilder.android.kt b/shared/data/src/androidMain/kotlin/fr/androidmakers/store/graphql/ApolloClientBuilder.android.kt index 985a5834..111b8a78 100644 --- a/shared/data/src/androidMain/kotlin/fr/androidmakers/store/graphql/ApolloClientBuilder.android.kt +++ b/shared/data/src/androidMain/kotlin/fr/androidmakers/store/graphql/ApolloClientBuilder.android.kt @@ -1,7 +1,6 @@ package fr.androidmakers.store.graphql import android.content.Context -import android.os.Build import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.api.http.HttpRequest import com.apollographql.apollo3.api.http.HttpResponse @@ -12,7 +11,6 @@ import com.apollographql.apollo3.network.http.HttpInterceptor import com.apollographql.apollo3.network.http.HttpInterceptorChain import com.google.firebase.auth.ktx.auth import com.google.firebase.ktx.Firebase -import kotlinx.coroutines.runBlocking actual class ApolloClientBuilder( context: Context, @@ -29,9 +27,7 @@ actual class ApolloClientBuilder( request.newBuilder() .addHeader("conference", conference) .apply { - val token = runBlocking { - Firebase.auth.currentUser?.getIdToken(false)?.result?.token - } + val token = Firebase.auth.currentUser?.getIdToken(false)?.result?.token if (token != null) { addHeader("Authorization", "Bearer $token") } diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/firebase/FirebaseUserRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/firebase/FirebaseUserRepository.kt index 46955750..418d987b 100644 --- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/firebase/FirebaseUserRepository.kt +++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/firebase/FirebaseUserRepository.kt @@ -6,7 +6,7 @@ import fr.androidmakers.domain.model.User import fr.androidmakers.domain.repo.UserRepository class FirebaseUserRepository : UserRepository { - override suspend fun getUser(): User? { + override fun getUser(): User? { return try { Firebase.auth.currentUser?.toUser() } catch (e: Exception) { diff --git a/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/repo/UserRepository.kt b/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/repo/UserRepository.kt index 7d034e85..92b91e84 100644 --- a/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/repo/UserRepository.kt +++ b/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/repo/UserRepository.kt @@ -3,5 +3,5 @@ package fr.androidmakers.domain.repo import fr.androidmakers.domain.model.User interface UserRepository { - suspend fun getUser(): User? + fun getUser(): User? } diff --git a/wearApp/build.gradle.kts b/wearApp/build.gradle.kts index cff00ad9..14181405 100644 --- a/wearApp/build.gradle.kts +++ b/wearApp/build.gradle.kts @@ -2,6 +2,8 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.google.services) + + alias(libs.plugins.androidmakers.android.signing) } android { @@ -9,6 +11,7 @@ android { compileSdk = 34 defaultConfig { + applicationId = "fr.paug.androidmakers" minSdk = 30 targetSdk = 34 versionCode = 1 @@ -18,13 +21,6 @@ android { } } - buildTypes { - release { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") - } - } - compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 @@ -52,6 +48,7 @@ android { dependencies { implementation(libs.play.services.wearable) + implementation(libs.play.services.auth) implementation(libs.androidx.activity.compose) implementation(libs.androidx.splashscreen) implementation(libs.wear.compose.material) @@ -60,13 +57,17 @@ dependencies { implementation(libs.horologist.composables) implementation(libs.horologist.compose.layout) implementation(libs.horologist.compose.material) + implementation(libs.horologist.auth.ui) implementation(libs.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.tooling) implementation(libs.wear.compose.navigation) + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.auth.ktx) coreLibraryDesugaring(libs.desugar.jdk.libs) debugImplementation(libs.compose.ui.tooling) implementation(libs.koin.android) + implementation(libs.koin.androidx.compose) implementation(project(":shared:di")) implementation(project(":shared:domain")) } diff --git a/wearApp/google-services.json b/wearApp/google-services.json deleted file mode 100644 index 5eb612f0..00000000 --- a/wearApp/google-services.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "project_info": { - "project_number": "127852231544", - "project_id": "androidmakers-2023", - "storage_bucket": "androidmakers-2023.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:127852231544:android:36ae3551145453a9e6e68d", - "android_client_info": { - "package_name": "fr.paug.androidmakers.wear" - } - }, - "oauth_client": [ - { - "client_id": "127852231544-bmorteqbdjjjffq7uirf6i8db0t4bo71.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "fr.paug.androidmakers.wear", - "certificate_hash": "a1eb323c4b06a569e8db487bb03a65d2ea649f48" - } - }, - { - "client_id": "127852231544-nislnj6eo41sbj5f9mmbeedcqdrk1pib.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "fr.paug.androidmakers.wear", - "certificate_hash": "ff357f347e107c052e57095034000d9a312275a5" - } - }, - { - "client_id": "127852231544-1nj0bjn8v2h7psd1lnf8ppovoe3k245b.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyD0MDezelAjCX4IaK2Me-NSX0GSKFmMxHc" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "127852231544-119kptis2pedq9rav7s8jto1mlnp208o.apps.googleusercontent.com", - "client_type": 3 - } - ] - } - } - } - ], - "configuration_version": "1" -} diff --git a/wearApp/google-services.json b/wearApp/google-services.json new file mode 120000 index 00000000..a5424d48 --- /dev/null +++ b/wearApp/google-services.json @@ -0,0 +1 @@ +../androidApp/google-services.json \ No newline at end of file diff --git a/wearApp/keystore.debug b/wearApp/keystore.debug new file mode 120000 index 00000000..50ad1aca --- /dev/null +++ b/wearApp/keystore.debug @@ -0,0 +1 @@ +../androidApp/keystore.debug \ No newline at end of file diff --git a/wearApp/keystore.release b/wearApp/keystore.release new file mode 120000 index 00000000..925ee114 --- /dev/null +++ b/wearApp/keystore.release @@ -0,0 +1 @@ +../androidApp/keystore.release \ No newline at end of file diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/AndroidMakersWearApplication.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/AndroidMakersWearApplication.kt index ac128e1a..e9a83974 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/AndroidMakersWearApplication.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/AndroidMakersWearApplication.kt @@ -1,12 +1,16 @@ package fr.paug.androidmakers.wear; import android.app.Application +import android.content.Context import fr.androidmakers.di.DependenciesBuilder import fr.paug.androidmakers.wear.di.androidViewModelModule +lateinit var applicationContext: Context + class AndroidMakersWearApplication : Application() { override fun onCreate() { super.onCreate() + fr.paug.androidmakers.wear.applicationContext = applicationContext DependenciesBuilder(this).inject( listOf(androidViewModelModule) ) diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt index a0d8d799..35df4e61 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt @@ -1,9 +1,11 @@ package fr.paug.androidmakers.wear.di import fr.paug.androidmakers.wear.ui.main.MainViewModel +import fr.paug.androidmakers.wear.ui.signin.SignInViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val androidViewModelModule = module { - viewModel { MainViewModel(get(), get()) } + viewModel { MainViewModel(get(), get(), get(), get()) } + viewModel { SignInViewModel(get(), get()) } } diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt index 5082043a..355fa12f 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt @@ -5,23 +5,25 @@ package fr.paug.androidmakers.wear.ui.main import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Build import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.wear.compose.foundation.ExperimentalWearFoundationApi import androidx.wear.compose.material.MaterialTheme @@ -45,23 +47,22 @@ import com.google.android.horologist.compose.material.Button import com.google.android.horologist.compose.material.Chip import com.google.android.horologist.compose.material.ListHeaderDefaults.firstItemPadding import com.google.android.horologist.compose.material.ResponsiveListHeader -import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll +import com.google.android.horologist.compose.pager.PagerScreen +import fr.androidmakers.domain.model.User import fr.paug.androidmakers.wear.R +import fr.paug.androidmakers.wear.ui.session.list.SessionListScreen +import fr.paug.androidmakers.wear.ui.signin.SignInScreen import fr.paug.androidmakers.wear.ui.theme.AndroidMakersWearTheme -import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.androidx.compose.koinViewModel + +private val TAG = MainActivity::class.java.simpleName class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - val viewModel: MainViewModel by viewModel() - viewModel.logAgenda() - setContent { installSplashScreen() - WearApp() - setTheme(android.R.style.Theme_DeviceDefault) } } @@ -70,47 +71,79 @@ class MainActivity : ComponentActivity() { @Composable fun WearApp() { val navController = rememberSwipeDismissableNavController() - AndroidMakersWearTheme { AppScaffold { - SwipeDismissableNavHost(navController = navController, startDestination = "menu") { - composable("menu") { - GreetingScreen( - "Android", - onShowList = { navController.navigate("list") } + SwipeDismissableNavHost(navController = navController, startDestination = Navigation.MAIN) { + composable(Navigation.MAIN) { + MainScreen( + onSignInClick = { navController.navigate(Navigation.SIGN_IN) }, + onSignOutClick = { TODO() } ) } composable("list") { ListScreen() } + composable(Navigation.SIGN_IN) { + SignInScreen(onDismissOrTimeout = navController::popBackStack) + } } } } } + @Composable -fun GreetingScreen(greetingName: String, onShowList: () -> Unit) { - val scrollState = ScrollState(0) +fun MainScreen( + viewModel: MainViewModel = koinViewModel(), + onSignInClick: () -> Unit, + onSignOutClick: () -> Unit, +) { + ScreenScaffold { + val pagerState: PagerState = rememberPagerState(initialPage = 1, pageCount = { 3 }) + PagerScreen( + modifier = Modifier.fillMaxSize(), + state = pagerState + ) { page -> + when (page) { + 0 -> { + val user: User? by viewModel.user.collectAsState() + SettingsScreen( + user = user, + onSignInClick = onSignInClick, + onSignOutInClick = onSignOutClick, + ) + } - /* If you have enough items in your list, use [ScalingLazyColumn] which is an optimized - * version of LazyColumn for wear devices with some added features. For more information, - * see d.android.com/wear/compose. - */ - ScreenScaffold(scrollState = scrollState) { - val padding = ScalingLazyColumnDefaults.padding( - first = ItemType.Text, - last = ItemType.Chip - )() - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .rotaryWithScroll(scrollState) - .padding(padding), - verticalArrangement = Arrangement.Center - ) { - Greeting(greetingName = greetingName) - Chip(label = "Show List", onClick = onShowList) + 1 -> { + val sessionsDay1: List? by viewModel.sessionsDay1.collectAsState(initial = null) + SessionListScreen(sessions = sessionsDay1) + } + + 2 -> { + val sessionsDay2: List? by viewModel.sessionsDay2.collectAsState(initial = null) + SessionListScreen(sessions = sessionsDay2) + } + } + } + } +} + +@Composable +private fun SettingsScreen( + user: User?, + onSignInClick: () -> Unit, + onSignOutInClick: () -> Unit, +) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + if (user == null) { + Chip(label = stringResource(R.string.main_signIn), onClick = onSignInClick) + } else { + Chip(label = stringResource(R.string.main_signOut), onClick = onSignOutInClick) } } } @@ -179,7 +212,7 @@ fun Greeting(greetingName: String) { modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary, - text = stringResource(R.string.hello_world, greetingName) + text = "Hello" ) } } @@ -230,7 +263,10 @@ fun SampleDialogContent( @WearPreviewFontScales @Composable fun GreetingScreenPreview() { - GreetingScreen("Preview Android", onShowList = {}) + MainScreen( + onSignInClick = {}, + onSignOutClick = {}, + ) } @WearPreviewDevices diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt index dc4f282f..d977f878 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt @@ -1,21 +1,76 @@ package fr.paug.androidmakers.wear.ui.main import android.app.Application +import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import fr.androidmakers.domain.interactor.GetAgendaUseCase +import fr.androidmakers.domain.interactor.SyncBookmarksUseCase +import fr.androidmakers.domain.model.Agenda +import fr.androidmakers.domain.model.User +import fr.androidmakers.domain.repo.UserRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlinx.datetime.LocalDate +import java.time.Month + +private val TAG = MainViewModel::class.java.simpleName + +// TODO Update these dates with 2024 edition dates +private val DAY_1_DATE = LocalDate(year = 2023, month = Month.APRIL, dayOfMonth = 27) +private val DAY_2_DATE = LocalDate(year = 2023, month = Month.APRIL, dayOfMonth = 28) class MainViewModel( application: Application, + private val userRepository: UserRepository, private val getAgendaUseCase: GetAgendaUseCase, + val syncBookmarksUseCase: SyncBookmarksUseCase, ) : AndroidViewModel(application) { - fun logAgenda() { + private val _user = MutableStateFlow(null) + val user: StateFlow = _user + + init { viewModelScope.launch { - getAgendaUseCase().collect { - println(it.getOrNull()?.sessions) + _user.emit(userRepository.getUser()) + + val currentUser = _user.value + if (currentUser != null) { + // fire & forget + // This is racy but oh well... + syncBookmarksUseCase(currentUser.id) } } } + + private val sessions: Flow> = getAgendaUseCase() + .filter { it.isSuccess } + .map { it.getOrThrow().toSessionDetails() } + + + val sessionsDay1 = sessions.map { sessions -> sessions.filter { it.session.startsAt.date == DAY_1_DATE } } + + val sessionsDay2 = sessions.map { sessions -> sessions.filter { it.session.startsAt.date == DAY_2_DATE } } +} + +private fun Agenda.toSessionDetails(): List { + return sessions.values.mapNotNull { session -> + SessionDetails( + session = session, + speakers = session.speakers.map { + speakers[it] ?: run { + Log.d(TAG, "Speaker $it not found") + return@mapNotNull null + } + }, + room = rooms[session.roomId] ?: run { + Log.d(TAG, "Room ${session.roomId} not found") + return@mapNotNull null + } + ) + } } diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/Navigation.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/Navigation.kt new file mode 100644 index 00000000..318a8e75 --- /dev/null +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/Navigation.kt @@ -0,0 +1,6 @@ +package fr.paug.androidmakers.wear.ui.main + +object Navigation { + const val MAIN = "MAIN" + const val SIGN_IN = "SIGN_IN" +} diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/SessionDetails.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/SessionDetails.kt new file mode 100644 index 00000000..65e26d15 --- /dev/null +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/SessionDetails.kt @@ -0,0 +1,11 @@ +package fr.paug.androidmakers.wear.ui.main + +import fr.androidmakers.domain.model.Room +import fr.androidmakers.domain.model.Session +import fr.androidmakers.domain.model.Speaker + +data class SessionDetails( + val session: Session, + val speakers: List, + val room: Room, +) diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt new file mode 100644 index 00000000..2d1622b4 --- /dev/null +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt @@ -0,0 +1,66 @@ +@file:OptIn(ExperimentalHorologistApi::class) + +package fr.paug.androidmakers.wear.ui.session.list + +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.wear.compose.foundation.lazy.items +import androidx.wear.compose.material.CircularProgressIndicator +import androidx.wear.compose.material.Text +import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices +import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.compose.layout.ScalingLazyColumn +import com.google.android.horologist.compose.layout.rememberResponsiveColumnState +import com.google.android.horologist.compose.material.Chip +import com.google.android.horologist.compose.material.ListHeaderDefaults +import com.google.android.horologist.compose.material.ResponsiveListHeader +import fr.paug.androidmakers.wear.ui.main.SessionDetails + +@Composable +fun SessionListScreen(sessions: List?) { + if (sessions == null) { + Loading() + } else { + SessionList(sessions) + } +} + +@Composable +private fun SessionList(sessions: List) { + val columnState = rememberResponsiveColumnState() + ScalingLazyColumn( + columnState = columnState, + modifier = Modifier.fillMaxSize() + ) { + item { + ResponsiveListHeader(contentPadding = ListHeaderDefaults.firstItemPadding()) { + Text(text = "Day 1") // TODO + } + } + items(sessions, key = { it.session.id }) { session -> + Chip(label = session.session.title, onClick = { }) + } + } +} + +@Composable +private fun Loading() { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun SessionListScreenPreview() { + SessionListScreen(null) +} diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/GoogleSignInViewModel.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/GoogleSignInViewModel.kt new file mode 100644 index 00000000..f8d9de2c --- /dev/null +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/GoogleSignInViewModel.kt @@ -0,0 +1,43 @@ +package fr.paug.androidmakers.wear.ui.signin + +import android.util.Log +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.horologist.auth.data.googlesignin.GoogleSignInEventListener +import com.google.android.horologist.auth.ui.googlesignin.signin.GoogleSignInViewModel +import com.google.firebase.Firebase +import com.google.firebase.auth.FirebaseAuthException +import com.google.firebase.auth.GoogleAuthProvider +import com.google.firebase.auth.auth +import fr.paug.androidmakers.wear.R +import fr.paug.androidmakers.wear.applicationContext +import kotlinx.coroutines.tasks.await + +private val TAG = GoogleSignInViewModel::class.java.simpleName + +class GoogleSignInViewModel( + onSignInSuccess: () -> Unit, + onSignInFailed: () -> Unit, +) : GoogleSignInViewModel( + googleSignInClient = GoogleSignIn.getClient( + applicationContext, + GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken(applicationContext.getString(R.string.default_web_client_id)) + .build() + ), + googleSignInEventListener = object : GoogleSignInEventListener { + override suspend fun onSignedIn(account: GoogleSignInAccount) { + Log.d(TAG, "Google sign in success") + val idToken = account.idToken + try { + val credential = GoogleAuthProvider.getCredential(idToken!!, null) + Firebase.auth.signInWithCredential(credential).await() + onSignInSuccess() + } catch (e: FirebaseAuthException) { + Log.w(TAG, "Could not get Firebase auth credential from Google id token", e) + onSignInFailed() + } + } + } +) diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInScreen.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInScreen.kt new file mode 100644 index 00000000..41fd00c0 --- /dev/null +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInScreen.kt @@ -0,0 +1,41 @@ +package fr.paug.androidmakers.wear.ui.signin + +import android.util.Log +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.google.android.horologist.auth.composables.dialogs.SignedInConfirmationDialog +import com.google.android.horologist.auth.composables.screens.AuthErrorScreen +import com.google.android.horologist.auth.ui.googlesignin.signin.GoogleSignInScreen +import com.google.android.horologist.compose.layout.ScreenScaffold +import org.koin.androidx.compose.koinViewModel + +private const val TAG = "SignInScreen" + +@Composable +fun SignInScreen( + viewModel: SignInViewModel = koinViewModel(), + onDismissOrTimeout: () -> Unit, +) { + ScreenScaffold { + GoogleSignInScreen( + modifier = Modifier.fillMaxSize(), + onAuthCancelled = { + Log.d(TAG, "onAuthCancelled") + }, + failedContent = { + AuthErrorScreen() + }, + viewModel = GoogleSignInViewModel(onSignInSuccess = viewModel::onSignInSuccess, onSignInFailed = viewModel::onSignInFailed) + ) { successState -> + SignedInConfirmationDialog( + modifier = Modifier.fillMaxSize(), + onDismissOrTimeout = { + Log.d(TAG, "onDismissOrTimeout") + onDismissOrTimeout() + }, + accountUiModel = successState.accountUiModel, + ) + } + } +} diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInViewModel.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInViewModel.kt new file mode 100644 index 00000000..e67f441b --- /dev/null +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInViewModel.kt @@ -0,0 +1,26 @@ +package fr.paug.androidmakers.wear.ui.signin + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import fr.androidmakers.domain.interactor.SyncBookmarksUseCase +import fr.androidmakers.domain.repo.UserRepository +import kotlinx.coroutines.launch + +private val TAG = SignInViewModel::class.java.simpleName + +class SignInViewModel( + private val userRepository: UserRepository, + private val syncBookmarksUseCase: SyncBookmarksUseCase, +) : ViewModel() { + fun onSignInSuccess() { + val userId = userRepository.getUser()?.id ?: return + viewModelScope.launch { + syncBookmarksUseCase(userId) + } + } + + fun onSignInFailed() { + Log.w(TAG, "onSignInFailed") + } +} diff --git a/wearApp/src/main/res/values-fr/strings.xml b/wearApp/src/main/res/values-fr/strings.xml new file mode 100644 index 00000000..585d8971 --- /dev/null +++ b/wearApp/src/main/res/values-fr/strings.xml @@ -0,0 +1,4 @@ + + Connexion + Déconnexion + diff --git a/wearApp/src/main/res/values/strings.xml b/wearApp/src/main/res/values/strings.xml index b47e0764..2242af6f 100644 --- a/wearApp/src/main/res/values/strings.xml +++ b/wearApp/src/main/res/values/strings.xml @@ -1,4 +1,5 @@ - Android Makers by droidcon - Hello, World! + Android Makers by droidcon + Sign in + Sign out From 43211cd202722ab27b0f46e966745e182ed03f5a Mon Sep 17 00:00:00 2001 From: BoD Date: Sun, 24 Mar 2024 16:26:30 +0100 Subject: [PATCH 2/7] Use a TitleCard for the session items --- .../fr/androidmakers/store/graphql/mappers.kt | 6 +- .../fr/androidmakers/domain/model/Session.kt | 4 +- .../wear/ui/main/MainActivity.kt | 10 +- .../wear/ui/main/MainViewModel.kt | 32 +++- .../main/{SessionDetails.kt => UISession.kt} | 6 +- .../wear/ui/session/list/SessionListScreen.kt | 145 ++++++++++++++++-- wearApp/src/main/res/values-fr/strings.xml | 2 + wearApp/src/main/res/values/strings.xml | 2 + 8 files changed, 179 insertions(+), 28 deletions(-) rename wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/{SessionDetails.kt => UISession.kt} (69%) diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/mappers.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/mappers.kt index f444ddb3..a705405c 100644 --- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/mappers.kt +++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/mappers.kt @@ -1,14 +1,14 @@ package fr.androidmakers.store.graphql -import fr.androidmakers.store.graphql.fragment.RoomDetails -import fr.androidmakers.store.graphql.fragment.SessionDetails -import fr.androidmakers.store.graphql.fragment.SpeakerDetails import fr.androidmakers.domain.model.Complexity import fr.androidmakers.domain.model.Room import fr.androidmakers.domain.model.Session import fr.androidmakers.domain.model.SocialsItem import fr.androidmakers.domain.model.Speaker import fr.androidmakers.domain.model.Venue +import fr.androidmakers.store.graphql.fragment.RoomDetails +import fr.androidmakers.store.graphql.fragment.SessionDetails +import fr.androidmakers.store.graphql.fragment.SpeakerDetails fun SpeakerDetails.toSpeaker(): Speaker { return Speaker( diff --git a/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/model/Session.kt b/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/model/Session.kt index d1005d0f..1009905b 100644 --- a/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/model/Session.kt +++ b/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/model/Session.kt @@ -1,6 +1,8 @@ package fr.androidmakers.domain.model import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant import kotlin.time.Duration data class Session( @@ -16,11 +18,11 @@ data class Session( val platformUrl: String? = null, //TODO unsure val slidesUrl: String? = null, - val duration: Duration = Duration.ZERO, // TODO move to instant to handle timezone val startsAt: LocalDateTime, val endsAt: LocalDateTime, + val duration: Duration = endsAt.toInstant(TimeZone.UTC) - startsAt.toInstant(TimeZone.UTC), val roomId: String, val isServiceSession: Boolean, ) diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt index 355fa12f..6364ada2 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt @@ -99,7 +99,7 @@ fun MainScreen( onSignOutClick: () -> Unit, ) { ScreenScaffold { - val pagerState: PagerState = rememberPagerState(initialPage = 1, pageCount = { 3 }) + val pagerState: PagerState = rememberPagerState(initialPage = viewModel.getConferenceDay() + 1, pageCount = { 3 }) PagerScreen( modifier = Modifier.fillMaxSize(), state = pagerState @@ -115,13 +115,13 @@ fun MainScreen( } 1 -> { - val sessionsDay1: List? by viewModel.sessionsDay1.collectAsState(initial = null) - SessionListScreen(sessions = sessionsDay1) + val sessionsDay1: List? by viewModel.sessionsDay1.collectAsState(initial = null) + SessionListScreen(sessions = sessionsDay1, title = stringResource(id = R.string.main_day1)) } 2 -> { - val sessionsDay2: List? by viewModel.sessionsDay2.collectAsState(initial = null) - SessionListScreen(sessions = sessionsDay2) + val sessionsDay2: List? by viewModel.sessionsDay2.collectAsState(initial = null) + SessionListScreen(sessions = sessionsDay2, title = stringResource(id = R.string.main_day2)) } } } diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt index d977f878..f674db38 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt @@ -15,14 +15,19 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.plus +import kotlinx.datetime.todayIn import java.time.Month private val TAG = MainViewModel::class.java.simpleName -// TODO Update these dates with 2024 edition dates +// TODO Update this date with 2024 edition date! private val DAY_1_DATE = LocalDate(year = 2023, month = Month.APRIL, dayOfMonth = 27) -private val DAY_2_DATE = LocalDate(year = 2023, month = Month.APRIL, dayOfMonth = 28) +private val DAY_2_DATE = DAY_1_DATE.plus(1, DateTimeUnit.DAY) class MainViewModel( application: Application, @@ -30,8 +35,8 @@ class MainViewModel( private val getAgendaUseCase: GetAgendaUseCase, val syncBookmarksUseCase: SyncBookmarksUseCase, ) : AndroidViewModel(application) { - private val _user = MutableStateFlow(null) + val user: StateFlow = _user init { @@ -47,19 +52,30 @@ class MainViewModel( } } - private val sessions: Flow> = getAgendaUseCase() + private val sessions: Flow> = getAgendaUseCase() .filter { it.isSuccess } - .map { it.getOrThrow().toSessionDetails() } - + .map { it.getOrThrow().toUISessions() } val sessionsDay1 = sessions.map { sessions -> sessions.filter { it.session.startsAt.date == DAY_1_DATE } } val sessionsDay2 = sessions.map { sessions -> sessions.filter { it.session.startsAt.date == DAY_2_DATE } } + + /** + * Get the day of the conference, based on the current date. + * If the date is the first day of the conference or earlier, returns 0, otherwise returns 1. + */ + fun getConferenceDay(): Int { + return if (Clock.System.todayIn(TimeZone.currentSystemDefault()) <= DAY_1_DATE) { + 0 + } else { + 1 + } + } } -private fun Agenda.toSessionDetails(): List { +private fun Agenda.toUISessions(): List { return sessions.values.mapNotNull { session -> - SessionDetails( + UISession( session = session, speakers = session.speakers.map { speakers[it] ?: run { diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/SessionDetails.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/UISession.kt similarity index 69% rename from wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/SessionDetails.kt rename to wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/UISession.kt index 65e26d15..390fb6de 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/SessionDetails.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/UISession.kt @@ -4,8 +4,10 @@ import fr.androidmakers.domain.model.Room import fr.androidmakers.domain.model.Session import fr.androidmakers.domain.model.Speaker -data class SessionDetails( +data class UISession( val session: Session, val speakers: List, val room: Room, -) +) { + val formattedDuration: String = session.duration.inWholeMinutes.toString() + " min" +} diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt index 2d1622b4..b59f8cb0 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt @@ -3,34 +3,51 @@ package fr.paug.androidmakers.wear.ui.session.list import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.wear.compose.foundation.lazy.items import androidx.wear.compose.material.CircularProgressIndicator +import androidx.wear.compose.material.LocalContentColor +import androidx.wear.compose.material.LocalTextStyle +import androidx.wear.compose.material.MaterialTheme import androidx.wear.compose.material.Text +import androidx.wear.compose.material.TitleCard import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.compose.layout.ScalingLazyColumn import com.google.android.horologist.compose.layout.rememberResponsiveColumnState -import com.google.android.horologist.compose.material.Chip import com.google.android.horologist.compose.material.ListHeaderDefaults import com.google.android.horologist.compose.material.ResponsiveListHeader -import fr.paug.androidmakers.wear.ui.main.SessionDetails +import fr.androidmakers.domain.model.Room +import fr.androidmakers.domain.model.Session +import fr.androidmakers.domain.model.Speaker +import fr.paug.androidmakers.wear.R +import fr.paug.androidmakers.wear.ui.main.UISession +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.Month @Composable -fun SessionListScreen(sessions: List?) { +fun SessionListScreen(sessions: List?, title: String) { if (sessions == null) { Loading() } else { - SessionList(sessions) + SessionList(sessions, title) } } @Composable -private fun SessionList(sessions: List) { +private fun SessionList(sessions: List, title: String) { val columnState = rememberResponsiveColumnState() ScalingLazyColumn( columnState = columnState, @@ -38,11 +55,59 @@ private fun SessionList(sessions: List) { ) { item { ResponsiveListHeader(contentPadding = ListHeaderDefaults.firstItemPadding()) { - Text(text = "Day 1") // TODO + Text(text = title) } } items(sessions, key = { it.session.id }) { session -> - Chip(label = session.session.title, onClick = { }) + SessionItem(session) + } + } +} + +@Composable +private fun SessionItem(session: UISession) { + TitleCard( + modifier = Modifier.fillMaxWidth(), + title = { + Column( + modifier = Modifier.fillMaxWidth(), + ) { + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colors.onSurfaceVariant, + LocalTextStyle provides MaterialTheme.typography.caption1, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + ) { + Text( + modifier = Modifier.weight(1F), + text = session.session.startsAt.time.toString(), + ) + + Text( + text = session.formattedDuration, + ) + } + } + Spacer(modifier = Modifier.height(4.dp)) + Text(text = session.session.title) + } + }, + onClick = { /*TODO*/ }, + ) { + if (!session.session.isServiceSession) { + if (session.speakers.isNotEmpty()) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = session.speakers.joinToString { it.getFullNameAndCompany() }, + color = MaterialTheme.colors.primary, + ) + Spacer(modifier = Modifier.height(4.dp)) + } + Text( + text = session.room.name, + color = MaterialTheme.colors.secondary, + ) } } } @@ -61,6 +126,68 @@ private fun Loading() { @WearPreviewDevices @WearPreviewFontScales @Composable -fun SessionListScreenPreview() { - SessionListScreen(null) +private fun LoadingSessionListScreenPreview() { + SessionListScreen(null, stringResource(id = R.string.main_day1)) +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +private fun LoadedSessionListScreenPreview() { + SessionListScreen( + listOf( + UISession( + session = Session( + id = "1", + title = "Android Graphics: the Path to [UI] Riches", + description = "Android's graphics APIs are extensive and powerful... but maybe a little complicated. This session will show ways to use the graphics APIs to achieve cool effects and improve the visual quality and richness of your applications.", + roomId = "", + speakers = emptyList(), + startsAt = LocalDateTime(2023, Month.APRIL, 27, 9, 15), + endsAt = LocalDateTime(2023, Month.APRIL, 27, 10, 0), + isServiceSession = false, + ), + speakers = listOf( + Speaker( + id = "1", + name = "Speaker 1", + bio = "Bio 1", + ), + Speaker( + id = "2", + name = "Speaker 2", + bio = "Bio 2", + ) + ), + room = Room( + id = "1", + name = "Room 1" + ) + ), + UISession( + session = Session( + id = "2", + title = "Using Compose Runtime to create a client library", + description = "Jetpack Compose (UI) is a powerful UI toolkit for Android. Have you ever wondered where this power comes from? The answer is Compose Runtime. \r\n\r\nIn this talk, we will see how we can use Compose Runtime to create client libraries. Firstly, we will talk about Compose nodes, Composition, Recomposer, and how they are orchestrated to create a slot table. Then, we will see how the changes in the slot table are applied with an Applier. Moreover, we will touch upon the Snapshot system and how the changes in the state objects trigger a recomposition. Finally, we will create a basic UI toolkit for PowerPoint using Compose Runtime.", + roomId = "", + speakers = emptyList(), + startsAt = LocalDateTime(2023, Month.APRIL, 27, 10, 15), + endsAt = LocalDateTime(2023, Month.APRIL, 27, 11, 0), + isServiceSession = false, + ), + speakers = listOf( + Speaker( + id = "3", + name = "Speaker 3", + bio = "Bio 3", + ), + ), + room = Room( + id = "2", + name = "Room 2" + ) + ), + ), + stringResource(id = R.string.main_day1) + ) } diff --git a/wearApp/src/main/res/values-fr/strings.xml b/wearApp/src/main/res/values-fr/strings.xml index 585d8971..781d887b 100644 --- a/wearApp/src/main/res/values-fr/strings.xml +++ b/wearApp/src/main/res/values-fr/strings.xml @@ -1,4 +1,6 @@ Connexion Déconnexion + Jour 1 + Jour 2 diff --git a/wearApp/src/main/res/values/strings.xml b/wearApp/src/main/res/values/strings.xml index 2242af6f..68ce8261 100644 --- a/wearApp/src/main/res/values/strings.xml +++ b/wearApp/src/main/res/values/strings.xml @@ -2,4 +2,6 @@ Android Makers by droidcon Sign in Sign out + Day 1 + Day 2 From 647e0ce26acbb83d1c21cb1d32b1010297f3b535 Mon Sep 17 00:00:00 2001 From: BoD Date: Sun, 24 Mar 2024 17:26:00 +0100 Subject: [PATCH 3/7] Update colors --- .../wear/ui/session/list/SessionListScreen.kt | 2 +- .../paug/androidmakers/wear/ui/theme/Color.kt | 21 ++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt index b59f8cb0..fdce2250 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt @@ -100,7 +100,7 @@ private fun SessionItem(session: UISession) { Spacer(modifier = Modifier.height(2.dp)) Text( text = session.speakers.joinToString { it.getFullNameAndCompany() }, - color = MaterialTheme.colors.primary, + color = MaterialTheme.colors.primaryVariant, ) Spacer(modifier = Modifier.height(4.dp)) } diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/theme/Color.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/theme/Color.kt index db5567c3..a8f71e7a 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/theme/Color.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/theme/Color.kt @@ -3,19 +3,16 @@ package fr.paug.androidmakers.wear.ui.theme import androidx.compose.ui.graphics.Color import androidx.wear.compose.material.Colors -val Purple200 = Color(0xFFBB86FC) -val Purple500 = Color(0xFF6200EE) -val Purple700 = Color(0xFF3700B3) -val Teal200 = Color(0xFF03DAC5) -val Red400 = Color(0xFFCF6679) +val DroidConBlue700 = Color(0xFF1c11ea) +val DroidConBlue200 = Color(0xFF8e88f6) +val DroidConTeal200 = Color(0xFF5adec1) +val DroidConOrange400 = Color(0xFFf27752) val wearColorPalette: Colors = Colors( - primary = Purple200, - primaryVariant = Purple700, - secondary = Teal200, - secondaryVariant = Teal200, - error = Red400, - onPrimary = Color.Black, + primary = DroidConBlue700, + primaryVariant = DroidConBlue200, + secondary = DroidConTeal200, + secondaryVariant = DroidConOrange400, + onPrimary = Color.White, onSecondary = Color.Black, - onError = Color.Black ) From fa28fa16cb1770ca87017c1bbcb59f496869980e Mon Sep 17 00:00:00 2001 From: BoD Date: Sun, 24 Mar 2024 17:56:31 +0100 Subject: [PATCH 4/7] Add bookmark icon --- gradle/libs.versions.toml | 2 +- wearApp/build.gradle.kts | 2 +- .../androidmakers/wear/di/ViewModelModule.kt | 2 +- .../wear/ui/main/MainViewModel.kt | 17 ++++++--- .../androidmakers/wear/ui/main/UISession.kt | 1 + .../wear/ui/session/list/SessionListScreen.kt | 35 +++++++++++++++++-- 6 files changed, 49 insertions(+), 10 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c2243064..cfc32e5a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -92,7 +92,7 @@ play-services-wearable = { group = "com.google.android.gms", name = "play-servic androidx-splashscreen = "androidx.core:core-splashscreen:1.0.1" wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "androidx-wear-compose" } wear-compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "androidx-wear-compose" } -androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "compose" } +androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" } horologist-composables = { module = "com.google.android.horologist:horologist-composables", version.ref = "horologist" } horologist-compose-layout = { module = "com.google.android.horologist:horologist-compose-layout", version.ref = "horologist" } horologist-compose-material = { module = "com.google.android.horologist:horologist-compose-material", version.ref = "horologist" } diff --git a/wearApp/build.gradle.kts b/wearApp/build.gradle.kts index 14181405..754f2e7c 100644 --- a/wearApp/build.gradle.kts +++ b/wearApp/build.gradle.kts @@ -53,7 +53,7 @@ dependencies { implementation(libs.androidx.splashscreen) implementation(libs.wear.compose.material) implementation(libs.wear.compose.foundation) - implementation(libs.androidx.material.icons.core) + implementation(libs.androidx.material.icons.extended) implementation(libs.horologist.composables) implementation(libs.horologist.compose.layout) implementation(libs.horologist.compose.material) diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt index 35df4e61..0e796ca8 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt @@ -6,6 +6,6 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val androidViewModelModule = module { - viewModel { MainViewModel(get(), get(), get(), get()) } + viewModel { MainViewModel(get(), get(), get(), get(), get()) } viewModel { SignInViewModel(get(), get()) } } diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt index f674db38..3ad741de 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt @@ -5,6 +5,7 @@ import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import fr.androidmakers.domain.interactor.GetAgendaUseCase +import fr.androidmakers.domain.interactor.GetFavoriteSessionsUseCase import fr.androidmakers.domain.interactor.SyncBookmarksUseCase import fr.androidmakers.domain.model.Agenda import fr.androidmakers.domain.model.User @@ -12,6 +13,7 @@ import fr.androidmakers.domain.repo.UserRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -32,8 +34,9 @@ private val DAY_2_DATE = DAY_1_DATE.plus(1, DateTimeUnit.DAY) class MainViewModel( application: Application, private val userRepository: UserRepository, - private val getAgendaUseCase: GetAgendaUseCase, - val syncBookmarksUseCase: SyncBookmarksUseCase, + getAgendaUseCase: GetAgendaUseCase, + syncBookmarksUseCase: SyncBookmarksUseCase, + getFavoriteSessionsUseCase: GetFavoriteSessionsUseCase, ) : AndroidViewModel(application) { private val _user = MutableStateFlow(null) @@ -54,7 +57,10 @@ class MainViewModel( private val sessions: Flow> = getAgendaUseCase() .filter { it.isSuccess } - .map { it.getOrThrow().toUISessions() } + .map { it.getOrThrow() } + .combine(getFavoriteSessionsUseCase()) { agenda, favoriteSessions -> + agenda.toUISessions(favoriteSessions) + } val sessionsDay1 = sessions.map { sessions -> sessions.filter { it.session.startsAt.date == DAY_1_DATE } } @@ -73,7 +79,7 @@ class MainViewModel( } } -private fun Agenda.toUISessions(): List { +private fun Agenda.toUISessions(favoriteSessions: Set): List { return sessions.values.mapNotNull { session -> UISession( session = session, @@ -86,7 +92,8 @@ private fun Agenda.toUISessions(): List { room = rooms[session.roomId] ?: run { Log.d(TAG, "Room ${session.roomId} not found") return@mapNotNull null - } + }, + isBookmarked = session.id in favoriteSessions, ) } } diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/UISession.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/UISession.kt index 390fb6de..b5e192c1 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/UISession.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/UISession.kt @@ -8,6 +8,7 @@ data class UISession( val session: Session, val speakers: List, val room: Room, + val isBookmarked: Boolean, ) { val formattedDuration: String = session.duration.inWholeMinutes.toString() + " min" } diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt index fdce2250..6b0469cf 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt @@ -9,14 +9,20 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Bookmark import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layout import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp import androidx.wear.compose.foundation.lazy.items import androidx.wear.compose.material.CircularProgressIndicator +import androidx.wear.compose.material.Icon import androidx.wear.compose.material.LocalContentColor import androidx.wear.compose.material.LocalTextStyle import androidx.wear.compose.material.MaterialTheme @@ -64,6 +70,19 @@ private fun SessionList(sessions: List, title: String) { } } +fun Modifier.matchRowSize(): Modifier { + return layout { measurable, constraints -> + if (constraints.maxHeight == Constraints.Infinity) { + layout(0, 0) {} + } else { + val placeable = measurable.measure(constraints) + layout(placeable.width, placeable.height) { + placeable.place(0, 0) + } + } + } +} + @Composable private fun SessionItem(session: UISession) { TitleCard( @@ -78,7 +97,17 @@ private fun SessionItem(session: UISession) { ) { Row( modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, ) { + if (session.isBookmarked) { + Icon( + modifier = Modifier.size(18.dp), + imageVector = Icons.Rounded.Bookmark, + tint = MaterialTheme.colors.secondaryVariant, + contentDescription = "Bookmarked" + ) + } + Text( modifier = Modifier.weight(1F), text = session.session.startsAt.time.toString(), @@ -162,7 +191,8 @@ private fun LoadedSessionListScreenPreview() { room = Room( id = "1", name = "Room 1" - ) + ), + isBookmarked = true, ), UISession( session = Session( @@ -185,7 +215,8 @@ private fun LoadedSessionListScreenPreview() { room = Room( id = "2", name = "Room 2" - ) + ), + isBookmarked = false, ), ), stringResource(id = R.string.main_day1) From bade75eaa440816cb1202882078bc22905de6fa4 Mon Sep 17 00:00:00 2001 From: BoD Date: Sun, 24 Mar 2024 19:02:18 +0100 Subject: [PATCH 5/7] Bookmarked session filter --- gradle/libs.versions.toml | 2 + wearApp/build.gradle.kts | 1 + .../wear/AndroidMakersWearApplication.kt | 6 +- .../wear/data/LocalPreferencesRepository.kt | 11 +++ .../paug/androidmakers/wear/di/DataModule.kt | 8 ++ .../androidmakers/wear/di/ViewModelModule.kt | 4 +- .../wear/ui/main/MainActivity.kt | 25 +----- .../wear/ui/main/MainViewModel.kt | 11 ++- .../wear/ui/session/list/SessionListScreen.kt | 42 ++++++---- .../wear/ui/settings/SettingsScreen.kt | 82 +++++++++++++++++++ .../wear/ui/settings/SettingsViewModel.kt | 17 ++++ wearApp/src/main/res/values-fr/strings.xml | 8 +- wearApp/src/main/res/values/strings.xml | 9 +- 13 files changed, 177 insertions(+), 49 deletions(-) create mode 100644 wearApp/src/main/java/fr/paug/androidmakers/wear/data/LocalPreferencesRepository.kt create mode 100644 wearApp/src/main/java/fr/paug/androidmakers/wear/di/DataModule.kt create mode 100644 wearApp/src/main/java/fr/paug/androidmakers/wear/ui/settings/SettingsScreen.kt create mode 100644 wearApp/src/main/java/fr/paug/androidmakers/wear/ui/settings/SettingsViewModel.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cfc32e5a..f5ad72f4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,6 +27,7 @@ androidx-wear-compose = "1.3.0" playServicesWearable = "18.1.0" compose-ui-tooling = "1.3.0" horologist = "0.6.5" +kprefs = "1.7.2" [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } @@ -101,6 +102,7 @@ compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" androidx-compose-ui-tooling = { module = "androidx.wear.compose:compose-ui-tooling", version.ref = "compose-ui-tooling" } wear-compose-navigation = { module = "androidx.wear.compose:compose-navigation", version.ref = "androidx-wear-compose" } desugar-jdk-libs = "com.android.tools:desugar_jdk_libs:2.0.4" +kprefs = { module = "org.jraf:kprefs", version.ref = "kprefs" } [plugins] android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } diff --git a/wearApp/build.gradle.kts b/wearApp/build.gradle.kts index 754f2e7c..a79ec65d 100644 --- a/wearApp/build.gradle.kts +++ b/wearApp/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { implementation(libs.wear.compose.navigation) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.auth.ktx) + implementation(libs.kprefs) coreLibraryDesugaring(libs.desugar.jdk.libs) debugImplementation(libs.compose.ui.tooling) diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/AndroidMakersWearApplication.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/AndroidMakersWearApplication.kt index e9a83974..6ad02ca8 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/AndroidMakersWearApplication.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/AndroidMakersWearApplication.kt @@ -4,6 +4,7 @@ import android.app.Application import android.content.Context import fr.androidmakers.di.DependenciesBuilder import fr.paug.androidmakers.wear.di.androidViewModelModule +import fr.paug.androidmakers.wear.di.dataModule lateinit var applicationContext: Context @@ -12,7 +13,10 @@ class AndroidMakersWearApplication : Application() { super.onCreate() fr.paug.androidmakers.wear.applicationContext = applicationContext DependenciesBuilder(this).inject( - listOf(androidViewModelModule) + listOf( + androidViewModelModule, + dataModule, + ) ) } } diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/data/LocalPreferencesRepository.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/data/LocalPreferencesRepository.kt new file mode 100644 index 00000000..9b0b21ab --- /dev/null +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/data/LocalPreferencesRepository.kt @@ -0,0 +1,11 @@ +package fr.paug.androidmakers.wear.data + +import android.content.Context +import kotlinx.coroutines.flow.MutableStateFlow +import org.jraf.android.kprefs.Prefs + +class LocalPreferencesRepository(applicationContext: Context) { + private val localPrefs = Prefs(applicationContext) + + val showOnlyBookmarkedSessions: MutableStateFlow by localPrefs.BooleanFlow(false) +} diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/di/DataModule.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/di/DataModule.kt new file mode 100644 index 00000000..82133329 --- /dev/null +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/di/DataModule.kt @@ -0,0 +1,8 @@ +package fr.paug.androidmakers.wear.di + +import fr.paug.androidmakers.wear.data.LocalPreferencesRepository +import org.koin.dsl.module + +val dataModule = module { + single { LocalPreferencesRepository(get()) } +} diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt index 0e796ca8..7eb2d7c1 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt @@ -1,11 +1,13 @@ package fr.paug.androidmakers.wear.di import fr.paug.androidmakers.wear.ui.main.MainViewModel +import fr.paug.androidmakers.wear.ui.settings.SettingsViewModel import fr.paug.androidmakers.wear.ui.signin.SignInViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val androidViewModelModule = module { - viewModel { MainViewModel(get(), get(), get(), get(), get()) } + viewModel { MainViewModel(get(), get(), get(), get(), get(), get()) } viewModel { SignInViewModel(get(), get()) } + viewModel { SettingsViewModel(get(), get()) } } diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt index 6364ada2..9efe18cd 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt @@ -5,10 +5,8 @@ package fr.paug.androidmakers.wear.ui.main import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons @@ -19,11 +17,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.wear.compose.foundation.ExperimentalWearFoundationApi import androidx.wear.compose.material.MaterialTheme @@ -51,6 +47,7 @@ import com.google.android.horologist.compose.pager.PagerScreen import fr.androidmakers.domain.model.User import fr.paug.androidmakers.wear.R import fr.paug.androidmakers.wear.ui.session.list.SessionListScreen +import fr.paug.androidmakers.wear.ui.settings.SettingsScreen import fr.paug.androidmakers.wear.ui.signin.SignInScreen import fr.paug.androidmakers.wear.ui.theme.AndroidMakersWearTheme import org.koin.androidx.compose.koinViewModel @@ -128,26 +125,6 @@ fun MainScreen( } } -@Composable -private fun SettingsScreen( - user: User?, - onSignInClick: () -> Unit, - onSignOutInClick: () -> Unit, -) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - contentAlignment = Alignment.Center - ) { - if (user == null) { - Chip(label = stringResource(R.string.main_signIn), onClick = onSignInClick) - } else { - Chip(label = stringResource(R.string.main_signOut), onClick = onSignOutInClick) - } - } -} - @Composable fun ListScreen() { var showDialog by remember { mutableStateOf(false) } diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt index 3ad741de..b6f982cf 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt @@ -10,6 +10,7 @@ import fr.androidmakers.domain.interactor.SyncBookmarksUseCase import fr.androidmakers.domain.model.Agenda import fr.androidmakers.domain.model.User import fr.androidmakers.domain.repo.UserRepository +import fr.paug.androidmakers.wear.data.LocalPreferencesRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -33,7 +34,8 @@ private val DAY_2_DATE = DAY_1_DATE.plus(1, DateTimeUnit.DAY) class MainViewModel( application: Application, - private val userRepository: UserRepository, + userRepository: UserRepository, + localPreferencesRepository: LocalPreferencesRepository, getAgendaUseCase: GetAgendaUseCase, syncBookmarksUseCase: SyncBookmarksUseCase, getFavoriteSessionsUseCase: GetFavoriteSessionsUseCase, @@ -61,6 +63,13 @@ class MainViewModel( .combine(getFavoriteSessionsUseCase()) { agenda, favoriteSessions -> agenda.toUISessions(favoriteSessions) } + .combine(localPreferencesRepository.showOnlyBookmarkedSessions) { sessions, showOnlyBookmarked -> + if (showOnlyBookmarked) { + sessions.filter { it.isBookmarked } + } else { + sessions + } + } val sessionsDay1 = sessions.map { sessions -> sessions.filter { it.session.startsAt.date == DAY_1_DATE } } diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt index 6b0469cf..e1a65622 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/session/list/SessionListScreen.kt @@ -16,9 +16,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.layout import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.wear.compose.foundation.lazy.items import androidx.wear.compose.material.CircularProgressIndicator @@ -44,16 +43,25 @@ import kotlinx.datetime.LocalDateTime import kotlinx.datetime.Month @Composable -fun SessionListScreen(sessions: List?, title: String) { +fun SessionListScreen( + sessions: List?, + title: String, +) { if (sessions == null) { Loading() } else { - SessionList(sessions, title) + SessionList( + sessions = sessions, + title = title, + ) } } @Composable -private fun SessionList(sessions: List, title: String) { +private fun SessionList( + sessions: List, + title: String, +) { val columnState = rememberResponsiveColumnState() ScalingLazyColumn( columnState = columnState, @@ -64,20 +72,18 @@ private fun SessionList(sessions: List, title: String) { Text(text = title) } } - items(sessions, key = { it.session.id }) { session -> - SessionItem(session) - } - } -} - -fun Modifier.matchRowSize(): Modifier { - return layout { measurable, constraints -> - if (constraints.maxHeight == Constraints.Infinity) { - layout(0, 0) {} + if (sessions.isEmpty()) { + item { + Text( + text = stringResource(id = R.string.session_list_noSessions), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.caption2, + ) + } } else { - val placeable = measurable.measure(constraints) - layout(placeable.width, placeable.height) { - placeable.place(0, 0) + items(sessions, key = { it.session.id }) { session -> + SessionItem(session) } } } diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/settings/SettingsScreen.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/settings/SettingsScreen.kt new file mode 100644 index 00000000..65d3a9eb --- /dev/null +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/settings/SettingsScreen.kt @@ -0,0 +1,82 @@ +@file:OptIn(ExperimentalHorologistApi::class) + +package fr.paug.androidmakers.wear.ui.settings + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.wear.compose.material.Chip +import androidx.wear.compose.material.Switch +import androidx.wear.compose.material.Text +import androidx.wear.compose.material.ToggleChip +import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices +import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.compose.layout.ScalingLazyColumn +import com.google.android.horologist.compose.layout.rememberResponsiveColumnState +import fr.androidmakers.domain.model.User +import fr.paug.androidmakers.wear.R +import org.koin.androidx.compose.koinViewModel + +@Composable +fun SettingsScreen( + viewModel: SettingsViewModel = koinViewModel(), + user: User?, + onSignInClick: () -> Unit, + onSignOutInClick: () -> Unit, +) { + val columnState = rememberResponsiveColumnState() + ScalingLazyColumn( + columnState = columnState, + modifier = Modifier.fillMaxSize() + ) { + item { + if (user == null) { + Chip( + modifier = Modifier.fillMaxWidth(), + label = { Text(stringResource(R.string.settings_signIn)) }, + onClick = onSignInClick + ) + } else { + Chip( + modifier = Modifier.fillMaxWidth(), + label = { Text(stringResource(R.string.settings_signOut)) }, + onClick = onSignOutInClick + ) + } + } + + item { + val showOnlyBookmarkedSessions: Boolean by viewModel.showOnlyBookmarkedSessions.collectAsStateWithLifecycle() + ToggleChip( + modifier = Modifier.fillMaxWidth(), + checked = showOnlyBookmarkedSessions, + onCheckedChange = { checked -> + viewModel.setShowOnlyBookmarkedSessions(checked) + }, + label = { Text(stringResource(R.string.settings_showBookmarksOnly)) }, + toggleControl = { + Switch( + checked = showOnlyBookmarkedSessions, + enabled = true, + ) + }, + ) + } + } +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +private fun SettingsScreenPreview() { + SettingsScreen( + user = null, + onSignInClick = {}, + onSignOutInClick = {}, + ) +} diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/settings/SettingsViewModel.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/settings/SettingsViewModel.kt new file mode 100644 index 00000000..ee2af75c --- /dev/null +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/settings/SettingsViewModel.kt @@ -0,0 +1,17 @@ +package fr.paug.androidmakers.wear.ui.settings + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import fr.paug.androidmakers.wear.data.LocalPreferencesRepository + +class SettingsViewModel( + application: Application, + private val localPreferencesRepository: LocalPreferencesRepository, +) : AndroidViewModel(application) { + + val showOnlyBookmarkedSessions = localPreferencesRepository.showOnlyBookmarkedSessions + + fun setShowOnlyBookmarkedSessions(showOnlyBookmarkedSessions: Boolean) { + localPreferencesRepository.showOnlyBookmarkedSessions.value = showOnlyBookmarkedSessions + } +} diff --git a/wearApp/src/main/res/values-fr/strings.xml b/wearApp/src/main/res/values-fr/strings.xml index 781d887b..09155a98 100644 --- a/wearApp/src/main/res/values-fr/strings.xml +++ b/wearApp/src/main/res/values-fr/strings.xml @@ -1,6 +1,10 @@ - Connexion - Déconnexion Jour 1 Jour 2 + + Aucune session favorite + + Connexion + Déconnexion + Sessions favorites uniquement diff --git a/wearApp/src/main/res/values/strings.xml b/wearApp/src/main/res/values/strings.xml index 68ce8261..5d284229 100644 --- a/wearApp/src/main/res/values/strings.xml +++ b/wearApp/src/main/res/values/strings.xml @@ -1,7 +1,12 @@ Android Makers by droidcon - Sign in - Sign out + Day 1 Day 2 + + No bookmarked sessions + + Sign in + Sign out + Show bookmarks only From f1df39448e4623f90c2c223509e57c703a25a856 Mon Sep 17 00:00:00 2001 From: BoD Date: Sun, 24 Mar 2024 19:37:26 +0100 Subject: [PATCH 6/7] Sign out --- .../wear/ui/main/MainActivity.kt | 176 ++---------------- .../wear/ui/main/MainViewModel.kt | 25 ++- .../wear/ui/settings/SettingsScreen.kt | 46 ++++- .../wear/ui/signin/SignInScreen.kt | 9 +- 4 files changed, 89 insertions(+), 167 deletions(-) diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt index 9efe18cd..197f295e 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt @@ -1,48 +1,22 @@ -@file:OptIn(ExperimentalHorologistApi::class, ExperimentalWearFoundationApi::class) - package fr.paug.androidmakers.wear.ui.main import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Build import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.wear.compose.foundation.ExperimentalWearFoundationApi -import androidx.wear.compose.material.MaterialTheme -import androidx.wear.compose.material.Text -import androidx.wear.compose.material.TitleCard -import androidx.wear.compose.material.dialog.Dialog import androidx.wear.compose.navigation.SwipeDismissableNavHost import androidx.wear.compose.navigation.composable import androidx.wear.compose.navigation.rememberSwipeDismissableNavController -import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices -import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales -import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.compose.layout.AppScaffold -import com.google.android.horologist.compose.layout.ScalingLazyColumn -import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults -import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults.ItemType import com.google.android.horologist.compose.layout.ScreenScaffold -import com.google.android.horologist.compose.layout.rememberResponsiveColumnState -import com.google.android.horologist.compose.material.AlertContent -import com.google.android.horologist.compose.material.Button -import com.google.android.horologist.compose.material.Chip -import com.google.android.horologist.compose.material.ListHeaderDefaults.firstItemPadding -import com.google.android.horologist.compose.material.ResponsiveListHeader import com.google.android.horologist.compose.pager.PagerScreen import fr.androidmakers.domain.model.User import fr.paug.androidmakers.wear.R @@ -52,8 +26,6 @@ import fr.paug.androidmakers.wear.ui.signin.SignInScreen import fr.paug.androidmakers.wear.ui.theme.AndroidMakersWearTheme import org.koin.androidx.compose.koinViewModel -private val TAG = MainActivity::class.java.simpleName - class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -66,7 +38,9 @@ class MainActivity : ComponentActivity() { } @Composable -fun WearApp() { +fun WearApp( + viewModel: MainViewModel = koinViewModel(), +) { val navController = rememberSwipeDismissableNavController() AndroidMakersWearTheme { AppScaffold { @@ -74,24 +48,26 @@ fun WearApp() { composable(Navigation.MAIN) { MainScreen( onSignInClick = { navController.navigate(Navigation.SIGN_IN) }, - onSignOutClick = { TODO() } + onSignOutClick = { viewModel.signOut() }, + viewModel = viewModel, ) } - composable("list") { - ListScreen() - } composable(Navigation.SIGN_IN) { - SignInScreen(onDismissOrTimeout = navController::popBackStack) + SignInScreen( + onSignInSuccess = { + viewModel.onSignInSuccess() + }, + onDismissOrTimeout = navController::popBackStack + ) } } } } } - @Composable fun MainScreen( - viewModel: MainViewModel = koinViewModel(), + viewModel: MainViewModel, onSignInClick: () -> Unit, onSignOutClick: () -> Unit, ) { @@ -124,131 +100,3 @@ fun MainScreen( } } } - -@Composable -fun ListScreen() { - var showDialog by remember { mutableStateOf(false) } - - /* - * Specifying the types of items that appear at the start and end of the list ensures that the - * appropriate padding is used. - */ - val columnState = rememberResponsiveColumnState( - contentPadding = ScalingLazyColumnDefaults.padding( - first = ItemType.Text, - last = ItemType.SingleButton - ) - ) - - ScreenScaffold(scrollState = columnState) { - /* - * The Horologist [ScalingLazyColumn] takes care of the horizontal and vertical - * padding for the list, so there is no need to specify it, as in the [GreetingScreen] - * composable. - */ - ScalingLazyColumn( - columnState = columnState, - modifier = Modifier - .fillMaxSize() - ) { - item { - ResponsiveListHeader(contentPadding = firstItemPadding()) { - Text(text = "Header") - } - } - item { - TitleCard(title = { Text("Example Title") }, onClick = { }) { - Text("Example Content\nMore Lines\nAnd More") - } - } - item { - Chip(label = "Example Chip", onClick = { }) - } - item { - Button( - imageVector = Icons.Default.Build, - contentDescription = "Example Button", - onClick = { showDialog = true } - ) - } - } - } - - SampleDialog( - showDialog = showDialog, - onDismiss = { showDialog = false }, - onCancel = {}, - onOk = {} - ) -} - -@Composable -fun Greeting(greetingName: String) { - ResponsiveListHeader(contentPadding = firstItemPadding()) { - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - color = MaterialTheme.colors.primary, - text = "Hello" - ) - } -} - -@Composable -fun SampleDialog( - showDialog: Boolean, - onDismiss: () -> Unit, - onCancel: () -> Unit, - onOk: () -> Unit -) { - val state = rememberResponsiveColumnState() - - Dialog( - showDialog = showDialog, - onDismissRequest = onDismiss, - scrollState = state.state - ) { - SampleDialogContent(onCancel, onDismiss, onOk) - } -} - -@Composable -fun SampleDialogContent( - onCancel: () -> Unit, - onDismiss: () -> Unit, - onOk: () -> Unit -) { - AlertContent( - icon = {}, - title = "Title", - onCancel = { - onCancel() - onDismiss() - }, - onOk = { - onOk() - onDismiss() - } - ) { - item { - Text(text = "An unknown error occurred during the request.") - } - } -} - -@WearPreviewDevices -@WearPreviewFontScales -@Composable -fun GreetingScreenPreview() { - MainScreen( - onSignInClick = {}, - onSignOutClick = {}, - ) -} - -@WearPreviewDevices -@WearPreviewFontScales -@Composable -fun ListScreenPreview() { - ListScreen() -} diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt index b6f982cf..0a921da5 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt @@ -4,12 +4,18 @@ import android.app.Application import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.firebase.auth.ktx.auth +import com.google.firebase.ktx.Firebase import fr.androidmakers.domain.interactor.GetAgendaUseCase import fr.androidmakers.domain.interactor.GetFavoriteSessionsUseCase import fr.androidmakers.domain.interactor.SyncBookmarksUseCase import fr.androidmakers.domain.model.Agenda import fr.androidmakers.domain.model.User import fr.androidmakers.domain.repo.UserRepository +import fr.paug.androidmakers.wear.R +import fr.paug.androidmakers.wear.applicationContext import fr.paug.androidmakers.wear.data.LocalPreferencesRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -34,7 +40,7 @@ private val DAY_2_DATE = DAY_1_DATE.plus(1, DateTimeUnit.DAY) class MainViewModel( application: Application, - userRepository: UserRepository, + private val userRepository: UserRepository, localPreferencesRepository: LocalPreferencesRepository, getAgendaUseCase: GetAgendaUseCase, syncBookmarksUseCase: SyncBookmarksUseCase, @@ -86,6 +92,23 @@ class MainViewModel( 1 } } + + fun onSignInSuccess() { + _user.value = userRepository.getUser() + } + + fun signOut() { + val googleSignInClient = GoogleSignIn.getClient( + applicationContext, + GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken(applicationContext.getString(R.string.default_web_client_id)) + .build() + ) + googleSignInClient.signOut() + googleSignInClient.revokeAccess() + Firebase.auth.signOut() + _user.value = null + } } private fun Agenda.toUISessions(favoriteSessions: Set): List { diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/settings/SettingsScreen.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/settings/SettingsScreen.kt index 65d3a9eb..e5cbb611 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/settings/SettingsScreen.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/settings/SettingsScreen.kt @@ -6,6 +6,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -13,11 +16,13 @@ import androidx.wear.compose.material.Chip import androidx.wear.compose.material.Switch import androidx.wear.compose.material.Text import androidx.wear.compose.material.ToggleChip +import androidx.wear.compose.material.dialog.Dialog import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.compose.layout.ScalingLazyColumn import com.google.android.horologist.compose.layout.rememberResponsiveColumnState +import com.google.android.horologist.compose.material.AlertContent import fr.androidmakers.domain.model.User import fr.paug.androidmakers.wear.R import org.koin.androidx.compose.koinViewModel @@ -29,6 +34,7 @@ fun SettingsScreen( onSignInClick: () -> Unit, onSignOutInClick: () -> Unit, ) { + var showSignOutConfirmDialog by remember { mutableStateOf(false) } val columnState = rememberResponsiveColumnState() ScalingLazyColumn( columnState = columnState, @@ -45,7 +51,9 @@ fun SettingsScreen( Chip( modifier = Modifier.fillMaxWidth(), label = { Text(stringResource(R.string.settings_signOut)) }, - onClick = onSignOutInClick + onClick = { + showSignOutConfirmDialog = true + } ) } } @@ -68,6 +76,42 @@ fun SettingsScreen( ) } } + + SignOutConfirmDialog( + showDialog = showSignOutConfirmDialog, + onOk = { + onSignOutInClick() + showSignOutConfirmDialog = false + }, + onCancel = { + showSignOutConfirmDialog = false + }, + ) +} + +@Composable +private fun SignOutConfirmDialog( + showDialog: Boolean, + onOk: () -> Unit, + onCancel: () -> Unit, +) { + Dialog( + showDialog = showDialog, + onDismissRequest = onCancel, + ) { + AlertContent( + onOk = { + onOk() + }, + onCancel = { + onCancel() + }, + ) { + item { + Text(text = "Sign out?") + } + } + } } @WearPreviewDevices diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInScreen.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInScreen.kt index 41fd00c0..349c35b7 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInScreen.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInScreen.kt @@ -15,6 +15,7 @@ private const val TAG = "SignInScreen" @Composable fun SignInScreen( viewModel: SignInViewModel = koinViewModel(), + onSignInSuccess: () -> Unit, onDismissOrTimeout: () -> Unit, ) { ScreenScaffold { @@ -26,7 +27,13 @@ fun SignInScreen( failedContent = { AuthErrorScreen() }, - viewModel = GoogleSignInViewModel(onSignInSuccess = viewModel::onSignInSuccess, onSignInFailed = viewModel::onSignInFailed) + viewModel = GoogleSignInViewModel( + onSignInSuccess = { + viewModel.onSignInSuccess() + onSignInSuccess() + }, + onSignInFailed = viewModel::onSignInFailed + ) ) { successState -> SignedInConfirmationDialog( modifier = Modifier.fillMaxSize(), From cdcb47c0702003dad62f7f02614caf6feb6a1e36 Mon Sep 17 00:00:00 2001 From: BoD Date: Sun, 24 Mar 2024 23:22:30 +0100 Subject: [PATCH 7/7] Avoid recomposition in SignIn and add activity icon --- .../androidmakers/wear/di/ViewModelModule.kt | 2 -- .../wear/ui/main/MainActivity.kt | 22 ++++++++----- .../wear/ui/main/MainViewModel.kt | 29 ++++++++++++------ .../wear/ui/signin/SignInScreen.kt | 7 ++--- .../wear/ui/signin/SignInViewModel.kt | 26 ---------------- wearApp/src/main/res/drawable/splash_icon.png | Bin 0 -> 9766 bytes wearApp/src/main/res/drawable/splash_icon.xml | 27 ---------------- .../main/res/mipmap-anydpi/ic_launcher.xml | 5 +++ .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 1404 -> 0 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 2591 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 982 -> 0 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1961 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 1900 -> 0 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 9766 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 2884 -> 0 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 19236 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 3844 -> 0 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 20264 bytes wearApp/src/main/res/values-fr/strings.xml | 2 +- wearApp/src/main/res/values/strings.xml | 4 +-- 20 files changed, 44 insertions(+), 80 deletions(-) delete mode 100644 wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInViewModel.kt create mode 100644 wearApp/src/main/res/drawable/splash_icon.png delete mode 100644 wearApp/src/main/res/drawable/splash_icon.xml create mode 100644 wearApp/src/main/res/mipmap-anydpi/ic_launcher.xml delete mode 100644 wearApp/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 wearApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png delete mode 100644 wearApp/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 wearApp/src/main/res/mipmap-mdpi/ic_launcher_foreground.png delete mode 100644 wearApp/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 wearApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png delete mode 100644 wearApp/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 wearApp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png delete mode 100644 wearApp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 wearApp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt index 7eb2d7c1..b7fb145c 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/di/ViewModelModule.kt @@ -2,12 +2,10 @@ package fr.paug.androidmakers.wear.di import fr.paug.androidmakers.wear.ui.main.MainViewModel import fr.paug.androidmakers.wear.ui.settings.SettingsViewModel -import fr.paug.androidmakers.wear.ui.signin.SignInViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val androidViewModelModule = module { viewModel { MainViewModel(get(), get(), get(), get(), get(), get()) } - viewModel { SignInViewModel(get(), get()) } viewModel { SettingsViewModel(get(), get()) } } diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt index 197f295e..67e5dea9 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainActivity.kt @@ -42,22 +42,27 @@ fun WearApp( viewModel: MainViewModel = koinViewModel(), ) { val navController = rememberSwipeDismissableNavController() + val onSignInClick: () -> Unit = { + navController.navigate(Navigation.SIGN_IN) + } + val onSignInDismissOrTimeout: () -> Unit = { + navController.popBackStack() + } + val onSignInSuccess = viewModel::onSignInSuccess AndroidMakersWearTheme { AppScaffold { SwipeDismissableNavHost(navController = navController, startDestination = Navigation.MAIN) { composable(Navigation.MAIN) { MainScreen( - onSignInClick = { navController.navigate(Navigation.SIGN_IN) }, + onSignInClick = onSignInClick, onSignOutClick = { viewModel.signOut() }, viewModel = viewModel, ) } composable(Navigation.SIGN_IN) { SignInScreen( - onSignInSuccess = { - viewModel.onSignInSuccess() - }, - onDismissOrTimeout = navController::popBackStack + onSignInSuccess = onSignInSuccess, + onDismissOrTimeout = onSignInDismissOrTimeout ) } } @@ -73,13 +78,16 @@ fun MainScreen( ) { ScreenScaffold { val pagerState: PagerState = rememberPagerState(initialPage = viewModel.getConferenceDay() + 1, pageCount = { 3 }) + val user: User? by viewModel.user.collectAsState() + val sessionsDay1: List? by viewModel.sessionsDay1.collectAsState(initial = null) + val sessionsDay2: List? by viewModel.sessionsDay2.collectAsState(initial = null) + PagerScreen( modifier = Modifier.fillMaxSize(), state = pagerState ) { page -> when (page) { 0 -> { - val user: User? by viewModel.user.collectAsState() SettingsScreen( user = user, onSignInClick = onSignInClick, @@ -88,12 +96,10 @@ fun MainScreen( } 1 -> { - val sessionsDay1: List? by viewModel.sessionsDay1.collectAsState(initial = null) SessionListScreen(sessions = sessionsDay1, title = stringResource(id = R.string.main_day1)) } 2 -> { - val sessionsDay2: List? by viewModel.sessionsDay2.collectAsState(initial = null) SessionListScreen(sessions = sessionsDay2, title = stringResource(id = R.string.main_day2)) } } diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt index 0a921da5..46c64645 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt @@ -19,10 +19,12 @@ import fr.paug.androidmakers.wear.applicationContext import fr.paug.androidmakers.wear.data.LocalPreferencesRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.DateTimeUnit @@ -43,7 +45,7 @@ class MainViewModel( private val userRepository: UserRepository, localPreferencesRepository: LocalPreferencesRepository, getAgendaUseCase: GetAgendaUseCase, - syncBookmarksUseCase: SyncBookmarksUseCase, + private val syncBookmarksUseCase: SyncBookmarksUseCase, getFavoriteSessionsUseCase: GetFavoriteSessionsUseCase, ) : AndroidViewModel(application) { private val _user = MutableStateFlow(null) @@ -53,17 +55,20 @@ class MainViewModel( init { viewModelScope.launch { _user.emit(userRepository.getUser()) + maybeSyncBookmarks() + } + } - val currentUser = _user.value - if (currentUser != null) { - // fire & forget - // This is racy but oh well... - syncBookmarksUseCase(currentUser.id) - } + private suspend fun maybeSyncBookmarks() { + val currentUser = _user.value + if (currentUser != null) { + Log.d(TAG, "Syncing bookmarks") + syncBookmarksUseCase(currentUser.id) + Log.d(TAG, "Bookmarks synced") } } - private val sessions: Flow> = getAgendaUseCase() + private val sessions: Flow?> = getAgendaUseCase() .filter { it.isSuccess } .map { it.getOrThrow() } .combine(getFavoriteSessionsUseCase()) { agenda, favoriteSessions -> @@ -76,10 +81,11 @@ class MainViewModel( sessions } } + .stateIn(viewModelScope, SharingStarted.Lazily, null) - val sessionsDay1 = sessions.map { sessions -> sessions.filter { it.session.startsAt.date == DAY_1_DATE } } + val sessionsDay1 = sessions.map { sessions -> sessions?.filter { it.session.startsAt.date == DAY_1_DATE } } - val sessionsDay2 = sessions.map { sessions -> sessions.filter { it.session.startsAt.date == DAY_2_DATE } } + val sessionsDay2 = sessions.map { sessions -> sessions?.filter { it.session.startsAt.date == DAY_2_DATE } } /** * Get the day of the conference, based on the current date. @@ -95,6 +101,9 @@ class MainViewModel( fun onSignInSuccess() { _user.value = userRepository.getUser() + viewModelScope.launch { + maybeSyncBookmarks() + } } fun signOut() { diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInScreen.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInScreen.kt index 349c35b7..8c74ce85 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInScreen.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInScreen.kt @@ -8,13 +8,11 @@ import com.google.android.horologist.auth.composables.dialogs.SignedInConfirmati import com.google.android.horologist.auth.composables.screens.AuthErrorScreen import com.google.android.horologist.auth.ui.googlesignin.signin.GoogleSignInScreen import com.google.android.horologist.compose.layout.ScreenScaffold -import org.koin.androidx.compose.koinViewModel private const val TAG = "SignInScreen" @Composable fun SignInScreen( - viewModel: SignInViewModel = koinViewModel(), onSignInSuccess: () -> Unit, onDismissOrTimeout: () -> Unit, ) { @@ -29,10 +27,11 @@ fun SignInScreen( }, viewModel = GoogleSignInViewModel( onSignInSuccess = { - viewModel.onSignInSuccess() onSignInSuccess() }, - onSignInFailed = viewModel::onSignInFailed + onSignInFailed = { + Log.d(TAG, "onSignInFailed") + } ) ) { successState -> SignedInConfirmationDialog( diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInViewModel.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInViewModel.kt deleted file mode 100644 index e67f441b..00000000 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/SignInViewModel.kt +++ /dev/null @@ -1,26 +0,0 @@ -package fr.paug.androidmakers.wear.ui.signin - -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import fr.androidmakers.domain.interactor.SyncBookmarksUseCase -import fr.androidmakers.domain.repo.UserRepository -import kotlinx.coroutines.launch - -private val TAG = SignInViewModel::class.java.simpleName - -class SignInViewModel( - private val userRepository: UserRepository, - private val syncBookmarksUseCase: SyncBookmarksUseCase, -) : ViewModel() { - fun onSignInSuccess() { - val userId = userRepository.getUser()?.id ?: return - viewModelScope.launch { - syncBookmarksUseCase(userId) - } - } - - fun onSignInFailed() { - Log.w(TAG, "onSignInFailed") - } -} diff --git a/wearApp/src/main/res/drawable/splash_icon.png b/wearApp/src/main/res/drawable/splash_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d978952c1459448cb9657595455afcb3f4013799 GIT binary patch literal 9766 zcmeHtWmsF?@^63up?GO=_ZA3Fad(Oomk`{w3GT&;7K#-s)ZbD?E5jJjjFCD01&_q0E9&X0GIcous;BRHy;47X$b&`X955e zp1I90B=1i|+8L|aYia_X-t!;;HpW8$);))De*iG30e{i%IeYdf%?o9ACEfTW-JJ?m!YWyR>{=IRa?_mg7&mxlN~ z|5wb*%=j-AFBd6hV@(}K1rL}VqX?J}%*QPKfRT|=5@u^JuB)j0kNN#birLZ2%Tt_} z*Voq<>?;WNfI0B;i;0Qx@(J(?2=Lr%@WB1uy{!Cr+~F+$2KnDOigs`tn3JcMlZQLw zUtB9|4{t9iX6C<+{?q<_onB7%|Mlb!|7TkF6XgAShL<1A$NL}bdsE53QgLmVlij`N zzxdMplK;~DzhwXTk>vgB{QtO^f6w$^()+1OKak}8PuQd%P(fF40RWayYKn6Dei%FE z{>ijP!66HRwGurQbuBfl8Z9Yn-*oUEVMpt;&6ntYjFUN3x#d%{>OiYnYlt{JPM1k9 zDgaS_+t3H$iSd2OWH!#I{FqXMmmbl@(SQLLvs5FAK(@fQ`2`m)+RiRdjt7qiD~i*J zcczLL+o0{cx0k_}A?-_7Cp0!fcoSzFzB5Pdhm!-L?u(bVyax#y3DqBILQfH)Kl?8ccbUKQ_qE>-Kb9oT zU~euhO?=NaRH}ts-^uSbc}W6PR?kSXAXp378I_PYgC1qXGhEr-WO$TioMm%DYℑ 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 - - - - - - - - - - - - - diff --git a/wearApp/src/main/res/mipmap-anydpi/ic_launcher.xml b/wearApp/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 00000000..728b6f22 --- /dev/null +++ b/wearApp/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/wearApp/src/main/res/mipmap-hdpi/ic_launcher.webp b/wearApp/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78ecd372343283f4157dcfd918ec5165bb3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG diff --git a/wearApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/wearApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..33007ea277eb5eb1ea7d8781dc6ec99de6bff0b3 GIT binary patch literal 2591 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 X&>Vb~ky}8J{PvJ`JJ^=l9FF}LomEuf literal 0 HcmV?d00001 diff --git a/wearApp/src/main/res/mipmap-mdpi/ic_launcher.webp b/wearApp/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d64e58ba64d180ce43ee13bf9a17835fbca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!T~Is8vkxtOvF+MFJ=)98*-esW`49~!-VWKzu62E+l~!0%CE@$I5@jUI685= z4jo0pXt&cXe^xdT9J>6UjcmoGO ztk*eE06573)Ydt{U{oy%=&G7eaZ@eivtliDgKb~C=`&W9S!+XdzyRk{f&>7tMNj1r zpt{BoR14w+GK37851z$K#6(5&m~4zFk*}%(2qHWP6WPKjq$n|gE5M6fP#+n15UbKy z6!IfQc+>^Op!p#W@RHa_XN*0@9!1ndB9Vlo=oowe>EL=fxN||p359$-7Mq%yib>sz z;U&dl9dI}t*4`28=x7Hr>;!3CVU);@E7fi%#pERmT~H`h zqmA|Ho9S)%sA~p#0hs&5-ej_Pxc^m2V@Je$ zg`v5x8(REpf&qHBy~5ml-!qzA{AMzQXDS64jxP&HGuddl8rIU#`+DEZNQT9QJYr@i zuG;;`zbl^Im-kpsgkxVfF`)wtXK52{U{W{WH!GI<*uJ^uDhe*sgm``+VhrX=oXasA z={!5u``q_w(Y01)6(;a$cdmWg_MeA~1JzPI0 zk~Gnsa!RciUWLj7ys1j~yMHZQcu(VRZV>#wEE6Z2_V2PkIt9<*Z)GGjhi51yz<33w z*W?#}yWJAfGZ%}M@2TIViaTg(#}i%+?dr(UDa_FSFkh``@ZPWO)X}YJNQo5`c@kP0 zte_UBiw#$Dt;;DP`Z^9odl~P?X|?^yt$fzl(3;VBO?!f|jHMj-rp0maaW;kK@}N+$ z_$ck7e&%X-ackwCXbq}Cp)Y?JZuDGnSDE%uLrX!tXvf;3h=2Mv63@GFf)z3T^V+xG zR)g(Yd2VGIJN(GG==!G;H#h-Nj_K+Yns>s^w7>4!E5&ht(R9prmz-J1kq+PcCRJcN z?I(|)zRAvmcGr!X7B=W64JXr(%U8P0ED$9QY!NfS$A<<#?NHv}xmoizo%_1 zV>IDHRvuH67@~RbC;#$+k>bwH=r|Hq`bH z(;I&lD_p~ite`o92dBIjMwqD5S0x&lCOeV*s*w+UT$dWQ&rnHUMqUXnpBgKlO7EF# z4#GjiQiM3?5ZNE`XIocR%*@>5V($@S_!)9Df9EQ{hH(6RaNnSjJNBHb*ZZQn8-&Hi z8aHDJgH&28y@s>Bc{Hr$?`>`BJwt>d^Ix^)(%gf(Ex662I-ZEFofFmm#Y;WWS?Oru z1?$x!ld=$G+`!Wsz3Xo~LP!@^)BIzHG}_xGduOrmp6YFdEQuL3F=tfHB!}g8)i>q@ zN|%+*r+f#K_eg%eo$ueeY4~wC$&c|EmI#wA%>+)R54G_R=&hDOZGyx4i6Q^|siYE- z@&H1pkE2psf$3EYn2#fs2mwmRQCI$CfT3(pyKhfK9uC8wNxm<*(&3yCrL9BW9 uxrJNpZp_0A29G?#-Z@7=zOZa{^t@U0l2&5sj9R$r2Sg?NlA1jtbN&Latrk}R literal 0 HcmV?d00001 diff --git a/wearApp/src/main/res/mipmap-xhdpi/ic_launcher.webp b/wearApp/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a3070fe34c611c42c0d3ad3013a0dce358be0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? diff --git a/wearApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/wearApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..d978952c1459448cb9657595455afcb3f4013799 GIT binary patch literal 9766 zcmeHtWmsF?@^63up?GO=_ZA3Fad(Oomk`{w3GT&;7K#-s)ZbD?E5jJjjFCD01&_q0E9&X0GIcous;BRHy;47X$b&`X955e zp1I90B=1i|+8L|aYia_X-t!;;HpW8$);))De*iG30e{i%IeYdf%?o9ACEfTW-JJ?m!YWyR>{=IRa?_mg7&mxlN~ z|5wb*%=j-AFBd6hV@(}K1rL}VqX?J}%*QPKfRT|=5@u^JuB)j0kNN#birLZ2%Tt_} z*Voq<>?;WNfI0B;i;0Qx@(J(?2=Lr%@WB1uy{!Cr+~F+$2KnDOigs`tn3JcMlZQLw zUtB9|4{t9iX6C<+{?q<_onB7%|Mlb!|7TkF6XgAShL<1A$NL}bdsE53QgLmVlij`N zzxdMplK;~DzhwXTk>vgB{QtO^f6w$^()+1OKak}8PuQd%P(fF40RWayYKn6Dei%FE z{>ijP!66HRwGurQbuBfl8Z9Yn-*oUEVMpt;&6ntYjFUN3x#d%{>OiYnYlt{JPM1k9 zDgaS_+t3H$iSd2OWH!#I{FqXMmmbl@(SQLLvs5FAK(@fQ`2`m)+RiRdjt7qiD~i*J zcczLL+o0{cx0k_}A?-_7Cp0!fcoSzFzB5Pdhm!-L?u(bVyax#y3DqBILQfH)Kl?8ccbUKQ_qE>-Kb9oT zU~euhO?=NaRH}ts-^uSbc}W6PR?kSXAXp378I_PYgC1qXGhEr-WO$TioMm%DYℑ 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 Déconnexion - Sessions favorites uniquement + Sessions favorites seulement diff --git a/wearApp/src/main/res/values/strings.xml b/wearApp/src/main/res/values/strings.xml index 5d284229..82bb8081 100644 --- a/wearApp/src/main/res/values/strings.xml +++ b/wearApp/src/main/res/values/strings.xml @@ -4,9 +4,9 @@ Day 1 Day 2 - No bookmarked sessions + No favorite sessions Sign in Sign out - Show bookmarks only + Show favorites only