Migrate Preferences to new architecture
This commit is contained in:
parent
7d65a4a1e7
commit
7edfcac62b
26 changed files with 399 additions and 174 deletions
|
|
@ -13,6 +13,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import io.element.android.x.architecture.createNode
|
import io.element.android.x.architecture.createNode
|
||||||
import io.element.android.x.architecture.viewmodel.viewModelSupportNode
|
import io.element.android.x.architecture.viewmodel.viewModelSupportNode
|
||||||
import io.element.android.x.features.messages.MessagesScreen
|
import io.element.android.x.features.messages.MessagesScreen
|
||||||
|
import io.element.android.x.features.preferences.PreferencesFlowNode
|
||||||
import io.element.android.x.features.roomlist.RoomListNode
|
import io.element.android.x.features.roomlist.RoomListNode
|
||||||
import io.element.android.x.matrix.core.RoomId
|
import io.element.android.x.matrix.core.RoomId
|
||||||
import io.element.android.x.matrix.core.SessionId
|
import io.element.android.x.matrix.core.SessionId
|
||||||
|
|
@ -34,6 +35,10 @@ class LoggedInFlowNode(
|
||||||
override fun onRoomClicked(roomId: RoomId) {
|
override fun onRoomClicked(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Messages(roomId))
|
backstack.push(NavTarget.Messages(roomId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSettingsClicked() {
|
||||||
|
backstack.push(NavTarget.Settings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
|
|
@ -42,6 +47,9 @@ class LoggedInFlowNode(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Messages(val roomId: RoomId) : NavTarget
|
data class Messages(val roomId: RoomId) : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object Settings : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
@ -55,6 +63,9 @@ class LoggedInFlowNode(
|
||||||
onBackPressed = { backstack.pop() }
|
onBackPressed = { backstack.pop() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
NavTarget.Settings -> {
|
||||||
|
PreferencesFlowNode(buildContext)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ dependencies {
|
||||||
implementation(project(":libraries:matrix"))
|
implementation(project(":libraries:matrix"))
|
||||||
implementation(project(":libraries:designsystem"))
|
implementation(project(":libraries:designsystem"))
|
||||||
implementation(project(":libraries:elementresources"))
|
implementation(project(":libraries:elementresources"))
|
||||||
implementation(libs.mavericks.compose)
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
androidTestImplementation(libs.test.junitext)
|
androidTestImplementation(libs.test.junitext)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package io.element.android.x.features.logout
|
||||||
|
|
||||||
|
sealed interface LogoutPreferenceEvents {
|
||||||
|
object Logout: LogoutPreferenceEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package io.element.android.x.features.logout
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import io.element.android.x.architecture.Async
|
||||||
|
import io.element.android.x.architecture.Presenter
|
||||||
|
import io.element.android.x.architecture.execute
|
||||||
|
import io.element.android.x.matrix.MatrixClient
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class LogoutPreferencePresenter @Inject constructor(private val matrixClient: MatrixClient) : Presenter<LogoutPreferenceState, LogoutPreferenceEvents> {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(events: Flow<LogoutPreferenceEvents>): LogoutPreferenceState {
|
||||||
|
val logoutAction: MutableState<Async<Unit>> = remember {
|
||||||
|
mutableStateOf(Async.Uninitialized)
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
events.collect { event ->
|
||||||
|
when (event) {
|
||||||
|
LogoutPreferenceEvents.Logout -> logout(logoutAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return LogoutPreferenceState(
|
||||||
|
logoutAction = logoutAction.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.logout(logoutAction: MutableState<Async<Unit>>) = launch {
|
||||||
|
suspend {
|
||||||
|
matrixClient.logout()
|
||||||
|
}.execute(logoutAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,15 +19,11 @@ package io.element.android.x.features.logout
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Logout
|
import androidx.compose.material.icons.filled.Logout
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.airbnb.mvrx.Loading
|
import io.element.android.x.architecture.Async
|
||||||
import com.airbnb.mvrx.Success
|
|
||||||
import com.airbnb.mvrx.compose.collectAsState
|
|
||||||
import com.airbnb.mvrx.compose.mavericksViewModel
|
|
||||||
import io.element.android.x.designsystem.ElementXTheme
|
import io.element.android.x.designsystem.ElementXTheme
|
||||||
import io.element.android.x.designsystem.components.ProgressDialog
|
import io.element.android.x.designsystem.components.ProgressDialog
|
||||||
import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog
|
||||||
|
|
@ -36,12 +32,12 @@ import io.element.android.x.designsystem.components.preferences.PreferenceText
|
||||||
import io.element.android.x.element.resources.R as ElementR
|
import io.element.android.x.element.resources.R as ElementR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LogoutPreference(
|
fun LogoutPreferenceView(
|
||||||
viewModel: LogoutViewModel = mavericksViewModel(),
|
state: LogoutPreferenceState,
|
||||||
onSuccessLogout: () -> Unit = { },
|
onLogoutClicked: () -> Unit = {},
|
||||||
|
onSuccessLogout: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val state: LogoutViewState by viewModel.collectAsState()
|
if (state.logoutAction is Async.Success) {
|
||||||
if (state.logoutAction is Success) {
|
|
||||||
onSuccessLogout()
|
onSuccessLogout()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +61,7 @@ fun LogoutPreference(
|
||||||
},
|
},
|
||||||
onSubmitClicked = {
|
onSubmitClicked = {
|
||||||
openDialog.value = false
|
openDialog.value = false
|
||||||
viewModel.logout()
|
onLogoutClicked()
|
||||||
},
|
},
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
openDialog.value = false
|
openDialog.value = false
|
||||||
|
|
@ -73,7 +69,7 @@ fun LogoutPreference(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.logoutAction is Loading) {
|
if (state.logoutAction is Async.Loading) {
|
||||||
ProgressDialog(text = "Login out...")
|
ProgressDialog(text = "Login out...")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +91,6 @@ fun LogoutPreferenceContent(
|
||||||
@Preview
|
@Preview
|
||||||
fun LogoutContentPreview() {
|
fun LogoutContentPreview() {
|
||||||
ElementXTheme(darkTheme = false) {
|
ElementXTheme(darkTheme = false) {
|
||||||
LogoutPreference()
|
LogoutPreferenceView(LogoutPreferenceState())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,10 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.x.features.logout
|
package io.element.android.x.features.logout
|
||||||
|
|
||||||
import com.airbnb.mvrx.Async
|
import io.element.android.x.architecture.Async
|
||||||
import com.airbnb.mvrx.MavericksState
|
|
||||||
import com.airbnb.mvrx.Uninitialized
|
|
||||||
|
|
||||||
data class LogoutViewState(
|
data class LogoutPreferenceState(
|
||||||
val logoutAction: Async<Unit> = Uninitialized,
|
val logoutAction: Async<Unit> = Async.Uninitialized,
|
||||||
) : MavericksState
|
)
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.x.features.logout
|
|
||||||
|
|
||||||
import com.airbnb.mvrx.MavericksViewModel
|
|
||||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import io.element.android.x.anvilannotations.ContributesViewModel
|
|
||||||
import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory
|
|
||||||
import io.element.android.x.di.SessionScope
|
|
||||||
import io.element.android.x.matrix.MatrixClient
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@ContributesViewModel(SessionScope::class)
|
|
||||||
class LogoutViewModel @AssistedInject constructor(
|
|
||||||
private val client: MatrixClient,
|
|
||||||
@Assisted initialState: LogoutViewState
|
|
||||||
) : MavericksViewModel<LogoutViewState>(initialState) {
|
|
||||||
|
|
||||||
companion object : MavericksViewModelFactory<LogoutViewModel, LogoutViewState> by daggerMavericksViewModelFactory()
|
|
||||||
|
|
||||||
fun logout() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
suspend {
|
|
||||||
client.logout()
|
|
||||||
}.execute {
|
|
||||||
copy(logoutAction = it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -20,6 +20,7 @@ plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
alias(libs.plugins.anvil)
|
alias(libs.plugins.anvil)
|
||||||
|
id("kotlin-parcelize")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package io.element.android.x.features.preferences
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.composable.Children
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.node.ParentNode
|
||||||
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import io.element.android.x.architecture.createNode
|
||||||
|
import io.element.android.x.features.preferences.root.PreferencesRootNode
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
class PreferencesFlowNode(
|
||||||
|
buildContext: BuildContext,
|
||||||
|
private val backstack: BackStack<NavTarget> = BackStack(
|
||||||
|
initialElement = NavTarget.Root,
|
||||||
|
savedStateMap = buildContext.savedStateMap,
|
||||||
|
),
|
||||||
|
) : ParentNode<PreferencesFlowNode.NavTarget>(
|
||||||
|
navModel = backstack,
|
||||||
|
buildContext = buildContext
|
||||||
|
) {
|
||||||
|
|
||||||
|
sealed interface NavTarget : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
object Root : NavTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
return when (navTarget) {
|
||||||
|
NavTarget.Root -> createNode<PreferencesRootNode>(buildContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
Children(navModel = backstack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.x.features.preferences
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import io.element.android.x.designsystem.components.preferences.PreferenceScreen
|
|
||||||
import io.element.android.x.element.resources.R as ElementR
|
|
||||||
import io.element.android.x.features.logout.LogoutPreference
|
|
||||||
import io.element.android.x.features.preferences.user.UserPreferences
|
|
||||||
import io.element.android.x.features.rageshake.preferences.RageshakePreferences
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun PreferencesScreen(
|
|
||||||
onBackPressed: () -> Unit = {},
|
|
||||||
onOpenRageShake: () -> Unit = {},
|
|
||||||
onSuccessLogout: () -> Unit = {},
|
|
||||||
) {
|
|
||||||
// TODO Hierarchy!
|
|
||||||
// Include pref from other modules
|
|
||||||
PreferencesContent(
|
|
||||||
onBackPressed = onBackPressed,
|
|
||||||
onOpenRageShake = onOpenRageShake,
|
|
||||||
onSuccessLogout = onSuccessLogout,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun PreferencesContent(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onBackPressed: () -> Unit = {},
|
|
||||||
onOpenRageShake: () -> Unit = {},
|
|
||||||
onSuccessLogout: () -> Unit = {},
|
|
||||||
) {
|
|
||||||
PreferenceScreen(
|
|
||||||
modifier = modifier,
|
|
||||||
onBackPressed = onBackPressed,
|
|
||||||
title = stringResource(id = ElementR.string.settings)
|
|
||||||
) {
|
|
||||||
UserPreferences()
|
|
||||||
RageshakePreferences(onOpenRageShake = onOpenRageShake)
|
|
||||||
LogoutPreference(onSuccessLogout = onSuccessLogout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun PreferencesContentPreview() {
|
|
||||||
PreferencesContent()
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package io.element.android.x.features.preferences.root
|
||||||
|
|
||||||
|
sealed interface PreferencesRootEvents {
|
||||||
|
object Logout : PreferencesRootEvents
|
||||||
|
data class SetRageshakeSensitivity(val sensitivity: Float) : PreferencesRootEvents
|
||||||
|
data class SetRageshakeEnabled(val enabled: Boolean) : PreferencesRootEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package io.element.android.x.features.preferences.root
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.x.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.x.architecture.presenterConnector
|
||||||
|
import io.element.android.x.di.SessionScope
|
||||||
|
|
||||||
|
@ContributesNode(SessionScope::class)
|
||||||
|
class PreferencesRootNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val presenter: PreferencesRootPresenter,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
|
private val presenterConnector = presenterConnector(presenter)
|
||||||
|
|
||||||
|
private fun onLogoutClicked() {
|
||||||
|
presenterConnector.emitEvent(PreferencesRootEvents.Logout)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRageshakeEnabledChanged(isEnabled: Boolean) {
|
||||||
|
presenterConnector.emitEvent(PreferencesRootEvents.SetRageshakeEnabled(isEnabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRageshakeSensitivityChanged(sensitivity: Float) {
|
||||||
|
presenterConnector.emitEvent(PreferencesRootEvents.SetRageshakeSensitivity(sensitivity))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
val state by presenterConnector.stateFlow.collectAsState()
|
||||||
|
PreferencesRootView(
|
||||||
|
state = state,
|
||||||
|
onLogoutClicked = this::onLogoutClicked,
|
||||||
|
onBackPressed = this::navigateUp,
|
||||||
|
onRageshakeEnabledChanged = this::onRageshakeEnabledChanged,
|
||||||
|
onRageshakeSensitivityChanged = this::onRageshakeSensitivityChanged
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package io.element.android.x.features.preferences.root
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import io.element.android.x.architecture.Async
|
||||||
|
import io.element.android.x.architecture.Presenter
|
||||||
|
import io.element.android.x.architecture.SharedFlowHolder
|
||||||
|
import io.element.android.x.features.logout.LogoutPreferenceEvents
|
||||||
|
import io.element.android.x.features.logout.LogoutPreferencePresenter
|
||||||
|
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents
|
||||||
|
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesPresenter
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class PreferencesRootPresenter @Inject constructor(
|
||||||
|
private val logoutPresenter: LogoutPreferencePresenter,
|
||||||
|
private val rageshakePresenter: RageshakePreferencesPresenter,
|
||||||
|
) : Presenter<PreferencesRootState, PreferencesRootEvents> {
|
||||||
|
|
||||||
|
private val logoutEventsFlow = SharedFlowHolder<LogoutPreferenceEvents>()
|
||||||
|
private val rageshakeEventsFlow = SharedFlowHolder<RageshakePreferencesEvents>()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(events: Flow<PreferencesRootEvents>): PreferencesRootState {
|
||||||
|
val logoutState = logoutPresenter.present(events = logoutEventsFlow.asSharedFlow())
|
||||||
|
val rageshakeState = rageshakePresenter.present(events = rageshakeEventsFlow.asSharedFlow())
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
events.collect { event ->
|
||||||
|
when (event) {
|
||||||
|
PreferencesRootEvents.Logout -> logoutEventsFlow.emit(LogoutPreferenceEvents.Logout)
|
||||||
|
is PreferencesRootEvents.SetRageshakeEnabled -> rageshakeEventsFlow.emit(RageshakePreferencesEvents.SetIsEnabled(event.enabled))
|
||||||
|
is PreferencesRootEvents.SetRageshakeSensitivity -> rageshakeEventsFlow.emit(RageshakePreferencesEvents.SetSensitivity(event.sensitivity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PreferencesRootState(
|
||||||
|
logoutState = logoutState,
|
||||||
|
rageshakeState = rageshakeState,
|
||||||
|
myUser = Async.Uninitialized
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package io.element.android.x.features.preferences.root
|
||||||
|
|
||||||
|
import io.element.android.x.architecture.Async
|
||||||
|
import io.element.android.x.features.logout.LogoutPreferenceState
|
||||||
|
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesState
|
||||||
|
import io.element.android.x.matrix.ui.model.MatrixUser
|
||||||
|
|
||||||
|
data class PreferencesRootState(
|
||||||
|
val logoutState: LogoutPreferenceState,
|
||||||
|
val rageshakeState: RageshakePreferencesState,
|
||||||
|
val myUser: Async<MatrixUser>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package io.element.android.x.features.preferences.root
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import io.element.android.x.architecture.Async
|
||||||
|
import io.element.android.x.designsystem.components.preferences.PreferenceView
|
||||||
|
import io.element.android.x.element.resources.R
|
||||||
|
import io.element.android.x.features.logout.LogoutPreferenceState
|
||||||
|
import io.element.android.x.features.logout.LogoutPreferenceView
|
||||||
|
import io.element.android.x.features.preferences.user.UserPreferences
|
||||||
|
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesState
|
||||||
|
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesView
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PreferencesRootView(
|
||||||
|
state: PreferencesRootState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onBackPressed: () -> Unit = {},
|
||||||
|
onLogoutClicked: () -> Unit = {},
|
||||||
|
onOpenRageShake: () -> Unit = {},
|
||||||
|
onRageshakeEnabledChanged: (Boolean) -> Unit = {},
|
||||||
|
onRageshakeSensitivityChanged: (Float) -> Unit = {},
|
||||||
|
) {
|
||||||
|
// TODO Hierarchy!
|
||||||
|
// Include pref from other modules
|
||||||
|
PreferenceView(
|
||||||
|
modifier = modifier,
|
||||||
|
onBackPressed = onBackPressed,
|
||||||
|
title = stringResource(id = R.string.settings)
|
||||||
|
) {
|
||||||
|
UserPreferences(state.myUser)
|
||||||
|
RageshakePreferencesView(
|
||||||
|
state = state.rageshakeState,
|
||||||
|
onOpenRageshake = onOpenRageShake,
|
||||||
|
onSensitivityChanged = onRageshakeSensitivityChanged,
|
||||||
|
onIsEnabledChanged = onRageshakeEnabledChanged,
|
||||||
|
)
|
||||||
|
LogoutPreferenceView(
|
||||||
|
state = state.logoutState,
|
||||||
|
onLogoutClicked = onLogoutClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreferencesContentPreview() {
|
||||||
|
val state = PreferencesRootState(
|
||||||
|
logoutState = LogoutPreferenceState(),
|
||||||
|
rageshakeState = RageshakePreferencesState(),
|
||||||
|
myUser = Async.Uninitialized
|
||||||
|
)
|
||||||
|
PreferencesRootView(state)
|
||||||
|
}
|
||||||
|
|
@ -19,26 +19,22 @@ package io.element.android.x.features.preferences.user
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.airbnb.mvrx.compose.collectAsState
|
import io.element.android.x.architecture.Async
|
||||||
import com.airbnb.mvrx.compose.mavericksViewModel
|
|
||||||
import io.element.android.x.matrix.ui.components.MatrixUserHeader
|
import io.element.android.x.matrix.ui.components.MatrixUserHeader
|
||||||
import io.element.android.x.matrix.ui.viewmodels.user.UserViewModel
|
import io.element.android.x.matrix.ui.model.MatrixUser
|
||||||
import io.element.android.x.matrix.ui.viewmodels.user.UserViewState
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserPreferences(
|
fun UserPreferences(
|
||||||
|
user: Async<MatrixUser>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: UserViewModel = mavericksViewModel(),
|
|
||||||
) {
|
) {
|
||||||
val user by viewModel.collectAsState(UserViewState::user)
|
when (val userData = user.dataOrNull()) {
|
||||||
when (user()) {
|
|
||||||
null -> Spacer(modifier = modifier.height(1.dp))
|
null -> Spacer(modifier = modifier.height(1.dp))
|
||||||
else -> MatrixUserHeader(
|
else -> MatrixUserHeader(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
matrixUser = user.invoke()!!
|
matrixUser = userData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package io.element.android.x.features.rageshake.preferences
|
||||||
|
|
||||||
|
sealed interface RageshakePreferencesEvents {
|
||||||
|
data class SetSensitivity(val sensitivity: Float) : RageshakePreferencesEvents
|
||||||
|
data class SetIsEnabled(val isEnabled: Boolean) : RageshakePreferencesEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
package io.element.android.x.features.rageshake.preferences
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import io.element.android.x.architecture.Presenter
|
||||||
|
import io.element.android.x.features.rageshake.rageshake.RageShake
|
||||||
|
import io.element.android.x.features.rageshake.rageshake.RageshakeDataStore
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class RageshakePreferencesPresenter @Inject constructor(
|
||||||
|
private val rageshake: RageShake,
|
||||||
|
private val rageshakeDataStore: RageshakeDataStore,
|
||||||
|
|
||||||
|
) : Presenter<RageshakePreferencesState, RageshakePreferencesEvents> {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(events: Flow<RageshakePreferencesEvents>): RageshakePreferencesState {
|
||||||
|
val isSupported: MutableState<Boolean> = rememberSaveable {
|
||||||
|
mutableStateOf(rageshake.isAvailable())
|
||||||
|
}
|
||||||
|
val isEnabled = rageshakeDataStore
|
||||||
|
.isEnabled()
|
||||||
|
.collectAsState(initial = false)
|
||||||
|
|
||||||
|
val sensitivity = rageshakeDataStore
|
||||||
|
.sensitivity()
|
||||||
|
.collectAsState(initial = 0f)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
events.collect { event ->
|
||||||
|
when (event) {
|
||||||
|
is RageshakePreferencesEvents.SetIsEnabled -> setIsEnabled(event.isEnabled)
|
||||||
|
is RageshakePreferencesEvents.SetSensitivity -> setSensitivity(event.sensitivity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RageshakePreferencesState(
|
||||||
|
isEnabled = isEnabled.value,
|
||||||
|
isSupported = isSupported.value,
|
||||||
|
sensitivity = sensitivity.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.setSensitivity(sensitivity: Float) = launch {
|
||||||
|
rageshakeDataStore.setSensitivity(sensitivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.setIsEnabled(enabled: Boolean) = launch {
|
||||||
|
rageshakeDataStore.setIsEnabled(enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package io.element.android.x.features.rageshake.preferences
|
||||||
|
|
||||||
|
data class RageshakePreferencesState(
|
||||||
|
val isEnabled: Boolean = false,
|
||||||
|
val isSupported: Boolean = true,
|
||||||
|
val sensitivity: Float = 0.3f,
|
||||||
|
)
|
||||||
|
|
@ -20,42 +20,29 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.BugReport
|
import androidx.compose.material.icons.filled.BugReport
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.airbnb.mvrx.compose.collectAsState
|
|
||||||
import com.airbnb.mvrx.compose.mavericksViewModel
|
|
||||||
import io.element.android.x.designsystem.components.preferences.PreferenceCategory
|
import io.element.android.x.designsystem.components.preferences.PreferenceCategory
|
||||||
import io.element.android.x.designsystem.components.preferences.PreferenceSlide
|
import io.element.android.x.designsystem.components.preferences.PreferenceSlide
|
||||||
import io.element.android.x.designsystem.components.preferences.PreferenceSwitch
|
import io.element.android.x.designsystem.components.preferences.PreferenceSwitch
|
||||||
import io.element.android.x.designsystem.components.preferences.PreferenceText
|
import io.element.android.x.designsystem.components.preferences.PreferenceText
|
||||||
import io.element.android.x.element.resources.R as ElementR
|
import io.element.android.x.element.resources.R as ElementR
|
||||||
import io.element.android.x.features.rageshake.detection.RageshakeDetectionViewModel
|
|
||||||
import io.element.android.x.features.rageshake.detection.RageshakeDetectionViewState
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RageshakePreferences(
|
fun RageshakePreferencesView(
|
||||||
onOpenRageShake: () -> Unit = {},
|
state: RageshakePreferencesState,
|
||||||
) {
|
|
||||||
RageshakePreferencesContent(
|
|
||||||
onOpenRageShake = onOpenRageShake,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RageshakePreferencesContent(
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: RageshakeDetectionViewModel = mavericksViewModel(),
|
onOpenRageshake: () -> Unit = {},
|
||||||
onOpenRageShake: () -> Unit = {},
|
onIsEnabledChanged: (Boolean) -> Unit = {},
|
||||||
|
onSensitivityChanged: (Float) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val state: RageshakeDetectionViewState by viewModel.collectAsState()
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
PreferenceCategory(title = stringResource(id = ElementR.string.send_bug_report)) {
|
PreferenceCategory(title = stringResource(id = ElementR.string.send_bug_report)) {
|
||||||
PreferenceText(
|
PreferenceText(
|
||||||
title = stringResource(id = ElementR.string.send_bug_report),
|
title = stringResource(id = ElementR.string.send_bug_report),
|
||||||
icon = Icons.Default.BugReport,
|
icon = Icons.Default.BugReport,
|
||||||
onClick = onOpenRageShake
|
onClick = onOpenRageshake
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
PreferenceCategory(title = stringResource(id = ElementR.string.settings_rageshake)) {
|
PreferenceCategory(title = stringResource(id = ElementR.string.settings_rageshake)) {
|
||||||
|
|
@ -63,7 +50,7 @@ fun RageshakePreferencesContent(
|
||||||
PreferenceSwitch(
|
PreferenceSwitch(
|
||||||
title = stringResource(id = ElementR.string.send_bug_report_rage_shake),
|
title = stringResource(id = ElementR.string.send_bug_report_rage_shake),
|
||||||
isChecked = state.isEnabled,
|
isChecked = state.isEnabled,
|
||||||
onCheckedChange = viewModel::onEnableClicked
|
onCheckedChange = onIsEnabledChanged
|
||||||
)
|
)
|
||||||
PreferenceSlide(
|
PreferenceSlide(
|
||||||
title = stringResource(id = ElementR.string.settings_rageshake_detection_threshold),
|
title = stringResource(id = ElementR.string.settings_rageshake_detection_threshold),
|
||||||
|
|
@ -71,7 +58,7 @@ fun RageshakePreferencesContent(
|
||||||
value = state.sensitivity,
|
value = state.sensitivity,
|
||||||
enabled = state.isEnabled,
|
enabled = state.isEnabled,
|
||||||
steps = 3 /* 5 possible values - steps are in ]0, 1[ */,
|
steps = 3 /* 5 possible values - steps are in ]0, 1[ */,
|
||||||
onValueChange = viewModel::onSensitivityChange
|
onValueChange = onSensitivityChanged
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
PreferenceText(title = "Rageshaking is not supported by your device")
|
PreferenceText(title = "Rageshaking is not supported by your device")
|
||||||
|
|
@ -82,6 +69,6 @@ fun RageshakePreferencesContent(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun RageshakePreferencePreview() {
|
fun RageshakePreferencesPreview() {
|
||||||
RageshakePreferences()
|
RageshakePreferencesView(RageshakePreferencesState(isEnabled = true, isSupported = true, sensitivity = 0.5f))
|
||||||
}
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ class RoomListNode @AssistedInject constructor(
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onRoomClicked(roomId: RoomId)
|
fun onRoomClicked(roomId: RoomId)
|
||||||
|
fun onSettingsClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val connector = presenterConnector(presenter)
|
private val connector = presenterConnector(presenter)
|
||||||
|
|
@ -45,6 +46,10 @@ class RoomListNode @AssistedInject constructor(
|
||||||
plugins<Callback>().forEach { it.onRoomClicked(roomId) }
|
plugins<Callback>().forEach { it.onRoomClicked(roomId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onOpenSettings() {
|
||||||
|
plugins<Callback>().forEach { it.onSettingsClicked() }
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state by connector.stateFlow.collectAsState()
|
val state by connector.stateFlow.collectAsState()
|
||||||
|
|
@ -53,7 +58,7 @@ class RoomListNode @AssistedInject constructor(
|
||||||
onRoomClicked = this::onRoomClicked,
|
onRoomClicked = this::onRoomClicked,
|
||||||
onFilterChanged = this::updateFilter,
|
onFilterChanged = this::updateFilter,
|
||||||
onScrollOver = this::updateVisibleRange,
|
onScrollOver = this::updateVisibleRange,
|
||||||
onOpenSettings = this::logout
|
onOpenSettings = this::onOpenSettings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
|
||||||
|
@Suppress("DSL_SCOPE_VIOLATION")
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
alias(libs.plugins.molecule)
|
alias(libs.plugins.molecule)
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,17 @@ import androidx.compose.runtime.MutableState
|
||||||
sealed interface Async<out T> {
|
sealed interface Async<out T> {
|
||||||
object Uninitialized : Async<Nothing>
|
object Uninitialized : Async<Nothing>
|
||||||
data class Loading<out T>(val prevState: T? = null) : Async<T>
|
data class Loading<out T>(val prevState: T? = null) : Async<T>
|
||||||
data class Failure<out T>(val error: Throwable) : Async<T>
|
data class Failure<out T>(val error: Throwable, val prevState: T? = null) : Async<T>
|
||||||
data class Success<out T>(val state: T) : Async<T>
|
data class Success<out T>(val state: T) : Async<T>
|
||||||
|
|
||||||
|
fun dataOrNull(): T? {
|
||||||
|
return when (this) {
|
||||||
|
is Failure -> prevState
|
||||||
|
is Loading -> prevState
|
||||||
|
is Success -> state
|
||||||
|
Uninitialized -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> (suspend () -> T).execute(state: MutableState<Async<T>>) {
|
suspend fun <T> (suspend () -> T).execute(state: MutableState<Async<T>>) {
|
||||||
|
|
|
||||||
|
|
@ -6,24 +6,21 @@ import app.cash.molecule.AndroidUiDispatcher
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.launchMolecule
|
import app.cash.molecule.launchMolecule
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
|
|
||||||
inline fun <reified State, reified Event> LifecycleOwner.presenterConnector(presenter: Presenter<State, Event>): LifecyclePresenterConnector<State, Event> =
|
inline fun <reified State, reified Event> LifecycleOwner.presenterConnector(presenter: Presenter<State, Event>): LifecyclePresenterConnector<State, Event> =
|
||||||
LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter)
|
LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter)
|
||||||
|
|
||||||
|
|
||||||
class LifecyclePresenterConnector<State, Event>(lifecycleOwner: LifecycleOwner, presenter: Presenter<State, Event>) {
|
class LifecyclePresenterConnector<State, Event>(lifecycleOwner: LifecycleOwner, presenter: Presenter<State, Event>) {
|
||||||
|
|
||||||
private val moleculeScope = CoroutineScope(lifecycleOwner.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main)
|
private val moleculeScope = CoroutineScope(lifecycleOwner.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main)
|
||||||
private val mutableEventFlow: MutableSharedFlow<Event> = MutableSharedFlow(extraBufferCapacity = 64)
|
private val eventFlow = SharedFlowHolder<Event>()
|
||||||
|
|
||||||
val stateFlow: StateFlow<State> = moleculeScope.launchMolecule(RecompositionClock.Immediate) {
|
val stateFlow: StateFlow<State> = moleculeScope.launchMolecule(RecompositionClock.Immediate) {
|
||||||
presenter.present(events = mutableEventFlow)
|
presenter.present(events = eventFlow.asSharedFlow())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun emitEvent(event: Event) {
|
fun emitEvent(event: Event) {
|
||||||
mutableEventFlow.tryEmit(event)
|
eventFlow.emit(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package io.element.android.x.architecture
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
|
||||||
|
class SharedFlowHolder<Data>(capacity: Int = 64) {
|
||||||
|
private val mutableFlow: MutableSharedFlow<Data> = MutableSharedFlow(extraBufferCapacity = capacity)
|
||||||
|
|
||||||
|
fun asSharedFlow() = mutableFlow.asSharedFlow()
|
||||||
|
|
||||||
|
fun emit(data: Data) = mutableFlow.tryEmit(data)
|
||||||
|
}
|
||||||
|
|
@ -45,7 +45,7 @@ import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PreferenceScreen(
|
fun PreferenceView(
|
||||||
title: String,
|
title: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit = {},
|
onBackPressed: () -> Unit = {},
|
||||||
|
|
@ -113,7 +113,7 @@ fun PreferenceTopAppBar(
|
||||||
@Composable
|
@Composable
|
||||||
@Preview(showBackground = false)
|
@Preview(showBackground = false)
|
||||||
fun PreferenceScreenPreview() {
|
fun PreferenceScreenPreview() {
|
||||||
PreferenceScreen(
|
PreferenceView(
|
||||||
title = "Preference screen"
|
title = "Preference screen"
|
||||||
) {
|
) {
|
||||||
PreferenceCategoryPreview()
|
PreferenceCategoryPreview()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue