diff --git a/data/db/src/commonMain/kotlin/io/github/droidkaigi/feeder/data/UserDataStore.kt b/data/db/src/commonMain/kotlin/io/github/droidkaigi/feeder/data/UserDataStore.kt index 899fa1b29..433eba71c 100644 --- a/data/db/src/commonMain/kotlin/io/github/droidkaigi/feeder/data/UserDataStore.kt +++ b/data/db/src/commonMain/kotlin/io/github/droidkaigi/feeder/data/UserDataStore.kt @@ -3,6 +3,7 @@ package io.github.droidkaigi.feeder.data import com.russhwolf.settings.MockSettings import com.russhwolf.settings.coroutines.FlowSettings import com.russhwolf.settings.coroutines.toFlowSettings +import io.github.droidkaigi.feeder.Theme import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -57,10 +58,22 @@ abstract class UserDataStore { val idToken: StateFlow = mutableIdToken suspend fun setIdToken(token: String) = mutableIdToken.emit(token) + fun theme(): Flow { + return flowSettings.getStringOrNullFlow(KEY_THEME).map { it?.let { Theme.valueOf(it) } } + } + + suspend fun changeTheme(theme: Theme) { + flowSettings.putString( + KEY_THEME, + theme.name + ) + } + companion object { private const val KEY_FAVORITES = "KEY_FAVORITES" private const val KEY_AUTHENTICATED = "KEY_AUTHENTICATED" private const val KEY_DEVICE_ID = "KEY_DEVICE_ID" + private const val KEY_THEME = "KEY_THEME" } } diff --git a/data/repository/src/commonMain/kotlin/io/github/droidkaigi/feeder/data/ThemeRepositoryImpl.kt b/data/repository/src/commonMain/kotlin/io/github/droidkaigi/feeder/data/ThemeRepositoryImpl.kt new file mode 100644 index 000000000..088a9a5c9 --- /dev/null +++ b/data/repository/src/commonMain/kotlin/io/github/droidkaigi/feeder/data/ThemeRepositoryImpl.kt @@ -0,0 +1,17 @@ +package io.github.droidkaigi.feeder.data + +import io.github.droidkaigi.feeder.Theme +import io.github.droidkaigi.feeder.repository.ThemeRepository +import kotlinx.coroutines.flow.Flow + +open class ThemeRepositoryImpl( + private val dataStore: UserDataStore, +) : ThemeRepository { + override suspend fun changeTheme(theme: Theme) { + dataStore.changeTheme(theme) + } + + override fun theme(): Flow { + return dataStore.theme() + } +} diff --git a/data/repository/src/main/java/io/github/droidkaigi/feeder/data/DaggerThemeRepositoryImpl.kt b/data/repository/src/main/java/io/github/droidkaigi/feeder/data/DaggerThemeRepositoryImpl.kt new file mode 100644 index 000000000..899f0a732 --- /dev/null +++ b/data/repository/src/main/java/io/github/droidkaigi/feeder/data/DaggerThemeRepositoryImpl.kt @@ -0,0 +1,11 @@ +package io.github.droidkaigi.feeder.data + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class DaggerThemeRepositoryImpl @Inject constructor( + dataDataStore: UserDataStore, +) : ThemeRepositoryImpl( + dataDataStore, +) diff --git a/data/repository/src/main/java/io/github/droidkaigi/feeder/data/DataModule.kt b/data/repository/src/main/java/io/github/droidkaigi/feeder/data/DataModule.kt index 027c66dbc..00c9b4f96 100644 --- a/data/repository/src/main/java/io/github/droidkaigi/feeder/data/DataModule.kt +++ b/data/repository/src/main/java/io/github/droidkaigi/feeder/data/DataModule.kt @@ -8,6 +8,7 @@ import io.github.droidkaigi.feeder.repository.ContributorRepository import io.github.droidkaigi.feeder.repository.DeviceRepository import io.github.droidkaigi.feeder.repository.FeedRepository import io.github.droidkaigi.feeder.repository.StaffRepository +import io.github.droidkaigi.feeder.repository.ThemeRepository @InstallIn(SingletonComponent::class) @Module @@ -67,4 +68,11 @@ class DataModule { ): ContributorRepository { return daggerRepository } + + @Provides + internal fun provideThemeRepository( + daggerRepository: DaggerThemeRepositoryImpl + ): ThemeRepository { + return daggerRepository + } } diff --git a/model/src/commonMain/kotlin/io/github/droidkaigi/feeder/Theme.kt b/model/src/commonMain/kotlin/io/github/droidkaigi/feeder/Theme.kt new file mode 100644 index 000000000..e7e91a15f --- /dev/null +++ b/model/src/commonMain/kotlin/io/github/droidkaigi/feeder/Theme.kt @@ -0,0 +1,7 @@ +package io.github.droidkaigi.feeder + +enum class Theme { + SYSTEM, + DARK, + LIGHT +} diff --git a/model/src/commonMain/kotlin/io/github/droidkaigi/feeder/repository/ThemeRepository.kt b/model/src/commonMain/kotlin/io/github/droidkaigi/feeder/repository/ThemeRepository.kt new file mode 100644 index 000000000..2c50992b2 --- /dev/null +++ b/model/src/commonMain/kotlin/io/github/droidkaigi/feeder/repository/ThemeRepository.kt @@ -0,0 +1,10 @@ +package io.github.droidkaigi.feeder.repository + +import io.github.droidkaigi.feeder.Theme +import kotlinx.coroutines.flow.Flow + +interface ThemeRepository { + suspend fun changeTheme(theme: Theme) + + fun theme(): Flow +} diff --git a/uicomponent-compose/core/src/main/java/io/github/droidkaigi/feeder/core/theme/Theme.kt b/uicomponent-compose/core/src/main/java/io/github/droidkaigi/feeder/core/theme/Theme.kt index 5a2815de5..112771d56 100644 --- a/uicomponent-compose/core/src/main/java/io/github/droidkaigi/feeder/core/theme/Theme.kt +++ b/uicomponent-compose/core/src/main/java/io/github/droidkaigi/feeder/core/theme/Theme.kt @@ -1,12 +1,14 @@ package io.github.droidkaigi.feeder.core.theme import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.darkColors import androidx.compose.material.lightColors import androidx.compose.runtime.Composable import dev.chrisbanes.accompanist.insets.ProvideWindowInsets +import io.github.droidkaigi.feeder.Theme private val DarkColorPalette = darkColors( primary = blue200, @@ -22,18 +24,13 @@ private val LightColorPalette = lightColors( @Composable fun ConferenceAppFeederTheme( - darkTheme: Boolean = isSystemInDarkTheme(), + theme: Theme? = Theme.SYSTEM, content: @Composable () -> Unit, ) { - val colors = if (darkTheme) { - DarkColorPalette - } else { - LightColorPalette - } ProvideWindowInsets { MaterialTheme( - colors = colors, + colors = colorPalette(theme), typography = typography, shapes = shapes, content = content @@ -41,13 +38,32 @@ fun ConferenceAppFeederTheme( } } +@Composable +private fun colorPalette(theme: Theme?): Colors { + return when (theme) { + Theme.SYSTEM -> systemColorPalette() + Theme.DARK -> DarkColorPalette + Theme.LIGHT -> LightColorPalette + else -> systemColorPalette() + } +} + +@Composable +private fun systemColorPalette(): Colors { + return if (isSystemInDarkTheme()) { + DarkColorPalette + } else { + LightColorPalette + } +} + @Composable fun AppThemeWithBackground( - darkTheme: Boolean = isSystemInDarkTheme(), + theme: Theme? = Theme.SYSTEM, content: @Composable () -> Unit, ) { Surface { - ConferenceAppFeederTheme(darkTheme, content) + ConferenceAppFeederTheme(theme, content) } } diff --git a/uicomponent-compose/core/src/main/java/io/github/droidkaigi/feeder/core/theme/ThemeTitle.kt b/uicomponent-compose/core/src/main/java/io/github/droidkaigi/feeder/core/theme/ThemeTitle.kt new file mode 100644 index 000000000..980cd7297 --- /dev/null +++ b/uicomponent-compose/core/src/main/java/io/github/droidkaigi/feeder/core/theme/ThemeTitle.kt @@ -0,0 +1,12 @@ +package io.github.droidkaigi.feeder.core.theme + +import android.content.Context +import io.github.droidkaigi.feeder.Theme +import io.github.droidkaigi.feeder.core.R + +fun Theme?.getTitle(context: Context): String = when (this) { + Theme.SYSTEM -> context.getString(R.string.system) + Theme.DARK -> context.getString(R.string.dark) + Theme.LIGHT -> context.getString(R.string.light) + else -> context.getString(R.string.system) +} diff --git a/uicomponent-compose/core/src/main/res/values-ja/strings.xml b/uicomponent-compose/core/src/main/res/values-ja/strings.xml index 5f432054a..14fb59206 100644 --- a/uicomponent-compose/core/src/main/res/values-ja/strings.xml +++ b/uicomponent-compose/core/src/main/res/values-ja/strings.xml @@ -3,4 +3,7 @@ ネットワークエラーが発生しました。もう一度お試しください。 サーバーエラーが発生しました。もう一度お試しください。 予期しないエラーが発生しました。もう一度お試しください。 + 端末設定に従う + ダーク + ライト diff --git a/uicomponent-compose/core/src/main/res/values/strings.xml b/uicomponent-compose/core/src/main/res/values/strings.xml index cfecafba1..f8a1103c3 100644 --- a/uicomponent-compose/core/src/main/res/values/strings.xml +++ b/uicomponent-compose/core/src/main/res/values/strings.xml @@ -3,4 +3,7 @@ A network error has occurred.Please try again. A server error has occurred.Please try again. An unknown error has occurred.Please try again. + Follow the device settings + Dark + Light diff --git a/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/AppViewModel.kt b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/AppViewModel.kt new file mode 100644 index 000000000..98a70e9b3 --- /dev/null +++ b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/AppViewModel.kt @@ -0,0 +1,43 @@ +package io.github.droidkaigi.feeder + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf +import io.github.droidkaigi.feeder.core.UnidirectionalViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +interface AppViewModel : + UnidirectionalViewModel< + AppViewModel.Event, + AppViewModel.Effect, + AppViewModel.State> { + data class State( + val theme: Theme? = Theme.SYSTEM, + ) + + sealed class Effect { + data class ErrorMessage(val appError: AppError) : Effect() + } + + sealed class Event + + override val state: StateFlow + override val effect: Flow + override fun event(event: Event) +} + +private val LocalAppViewModel = compositionLocalOf { + error("not LocalDroidKaigiAppViewModel provided") +} + +@Composable +fun ProvideAppViewModel( + viewModel: AppViewModel, + block: @Composable () -> Unit +) { + CompositionLocalProvider(LocalAppViewModel provides viewModel, content = block) +} + +@Composable +fun appViewModel() = LocalAppViewModel.current diff --git a/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/DroidKaigiApp.kt b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/DroidKaigiApp.kt index f1eb733e6..3e9c347cf 100644 --- a/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/DroidKaigiApp.kt +++ b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/DroidKaigiApp.kt @@ -13,37 +13,42 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import dev.chrisbanes.accompanist.insets.navigationBarsPadding import io.github.droidkaigi.feeder.core.theme.ConferenceAppFeederTheme +import io.github.droidkaigi.feeder.core.use @Composable fun DroidKaigiApp(firstSplashScreenState: SplashState = SplashState.Shown) { - ConferenceAppFeederTheme { - var splashShown by rememberSaveable { - mutableStateOf(firstSplashScreenState) - } - val transition = updateTransition(splashShown) - val splashAlpha: Float by transition.animateFloat( - transitionSpec = { tween(durationMillis = 100) } - ) { state -> - if (state == SplashState.Shown) 1f else 0f - } - val contentAlpha: Float by transition.animateFloat( - transitionSpec = { tween(durationMillis = 300) } - ) { state -> - if (state == SplashState.Shown) 0f else 1f - } + ProvideAppViewModel(viewModel = appViewModel()) { + val (state) = use(appViewModel()) + + ConferenceAppFeederTheme(state.theme) { + var splashShown by rememberSaveable { + mutableStateOf(firstSplashScreenState) + } + val transition = updateTransition(splashShown) + val splashAlpha: Float by transition.animateFloat( + transitionSpec = { tween(durationMillis = 100) } + ) { state -> + if (state == SplashState.Shown) 1f else 0f + } + val contentAlpha: Float by transition.animateFloat( + transitionSpec = { tween(durationMillis = 300) } + ) { state -> + if (state == SplashState.Shown) 0f else 1f + } - Box { - LandingScreen( - modifier = Modifier.alpha(splashAlpha), - ) { - splashShown = SplashState.Completed + Box { + LandingScreen( + modifier = Modifier.alpha(splashAlpha), + ) { + splashShown = SplashState.Completed + } } + AppContent( + modifier = Modifier + .alpha(contentAlpha) + .navigationBarsPadding(bottom = false) + ) } - AppContent( - modifier = Modifier - .alpha(contentAlpha) - .navigationBarsPadding(bottom = false) - ) } } diff --git a/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/LandingScreen.kt b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/LandingScreen.kt index e29d528f4..0f16dedb5 100644 --- a/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/LandingScreen.kt +++ b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/LandingScreen.kt @@ -33,7 +33,7 @@ fun LandingScreen(modifier: Modifier = Modifier, onTimeout: () -> Unit) { @Preview(showBackground = true) @Composable fun PreviewLandingScreen() { - ConferenceAppFeederTheme(false) { + ConferenceAppFeederTheme { LandingScreen( onTimeout = {} ) diff --git a/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/viewmodel/ProvideRealViewModels.kt b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/viewmodel/ProvideRealViewModels.kt index d0c8cbd63..6dcfec2c0 100644 --- a/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/viewmodel/ProvideRealViewModels.kt +++ b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/viewmodel/ProvideRealViewModels.kt @@ -2,18 +2,24 @@ package io.github.droidkaigi.feeder.viewmodel import androidx.compose.runtime.Composable import androidx.lifecycle.viewmodel.compose.viewModel +import io.github.droidkaigi.feeder.ProvideAppViewModel import io.github.droidkaigi.feeder.contributor.ProvideContributorViewModel import io.github.droidkaigi.feeder.contributor.fakeContributorViewModel import io.github.droidkaigi.feeder.feed.ProvideFeedViewModel +import io.github.droidkaigi.feeder.setting.ProvideSettingViewModel import io.github.droidkaigi.feeder.staff.ProvideStaffViewModel import io.github.droidkaigi.feeder.staff.fakeStaffViewModel @Composable fun ProvideViewModels(content: @Composable () -> Unit) { - ProvideFeedViewModel(viewModel()) { - ProvideStaffViewModel(viewModel = fakeStaffViewModel()) { - ProvideContributorViewModel(viewModel = fakeContributorViewModel()) { - content() + ProvideAppViewModel(viewModel()) { + ProvideFeedViewModel(viewModel()) { + ProvideSettingViewModel(viewModel()) { + ProvideStaffViewModel(viewModel = fakeStaffViewModel()) { + ProvideContributorViewModel(viewModel = fakeContributorViewModel()) { + content() + } + } } } } diff --git a/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/viewmodel/RealAppViewModel.kt b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/viewmodel/RealAppViewModel.kt new file mode 100644 index 000000000..b13364891 --- /dev/null +++ b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/viewmodel/RealAppViewModel.kt @@ -0,0 +1,33 @@ +package io.github.droidkaigi.feeder.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.droidkaigi.feeder.AppViewModel +import io.github.droidkaigi.feeder.repository.ThemeRepository +import javax.inject.Inject +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn + +@HiltViewModel +class RealAppViewModel @Inject constructor( + private val repository: ThemeRepository, +) : ViewModel(), AppViewModel { + private val effectChannel = Channel(Channel.UNLIMITED) + override val effect: Flow = effectChannel.receiveAsFlow() + + override val state: StateFlow = + repository.theme().map { AppViewModel.State(theme = it) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppViewModel.State() + ) + + override fun event(event: AppViewModel.Event) {} +} diff --git a/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/viewmodel/RealSettingViewModel.kt b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/viewmodel/RealSettingViewModel.kt new file mode 100644 index 000000000..9fb015046 --- /dev/null +++ b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/viewmodel/RealSettingViewModel.kt @@ -0,0 +1,44 @@ +package io.github.droidkaigi.feeder.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.cash.exhaustive.Exhaustive +import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.droidkaigi.feeder.repository.ThemeRepository +import io.github.droidkaigi.feeder.setting.SettingViewModel +import javax.inject.Inject +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +@HiltViewModel +class RealSettingViewModel @Inject constructor( + private val repository: ThemeRepository, +) : ViewModel(), SettingViewModel { + private val effectChannel = Channel(Channel.UNLIMITED) + override val effect: Flow = effectChannel.receiveAsFlow() + + override val state: StateFlow = + repository.theme().map { SettingViewModel.State(theme = it) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = SettingViewModel.State() + ) + + override fun event(event: SettingViewModel.Event) { + viewModelScope.launch { + @Exhaustive + when (event) { + is SettingViewModel.Event.ChangeTheme -> { + event.theme?.let { repository.changeTheme(theme = it) } + } + } + } + } +} diff --git a/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/contributor/ContributorItem.kt b/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/contributor/ContributorItem.kt index e02740579..05f766024 100644 --- a/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/contributor/ContributorItem.kt +++ b/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/contributor/ContributorItem.kt @@ -63,7 +63,7 @@ fun ContributorItem(contributor: Contributor, onClickItem: (Contributor) -> Unit @Preview(showBackground = true) @Composable fun PreviewContributorItem() { - ConferenceAppFeederTheme(false) { + ConferenceAppFeederTheme { val contributor = fakeContributors().first() ProvideContributorViewModel(viewModel = fakeContributorViewModel()) { ContributorItem(contributor = contributor) { diff --git a/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/contributor/ContributorList.kt b/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/contributor/ContributorList.kt index d17810739..f2353b965 100644 --- a/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/contributor/ContributorList.kt +++ b/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/contributor/ContributorList.kt @@ -44,7 +44,7 @@ fun ContributorList(onClickContributor: (Contributor) -> Unit) { @Preview(showBackground = true) @Composable fun PreviewContributorScreen() { - ConferenceAppFeederTheme(false) { + ConferenceAppFeederTheme { ProvideContributorViewModel(viewModel = fakeContributorViewModel()) { ContributorList() { } diff --git a/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/other/OtherScreen.kt b/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/other/OtherScreen.kt index 518d7bbcd..371b70ed9 100644 --- a/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/other/OtherScreen.kt +++ b/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/other/OtherScreen.kt @@ -39,6 +39,7 @@ import io.github.droidkaigi.feeder.core.TabIndicator import io.github.droidkaigi.feeder.core.TabRowDefaults.tabIndicatorOffset import io.github.droidkaigi.feeder.core.animation.FadeThrough import io.github.droidkaigi.feeder.core.theme.ConferenceAppFeederTheme +import io.github.droidkaigi.feeder.setting.Settings import io.github.droidkaigi.feeder.staff.StaffList sealed class OtherTabs(val name: String, val routePath: String) { @@ -165,6 +166,7 @@ private fun BackdropFrontLayerContent( when (selectedTab) { OtherTabs.AboutThisApp -> AboutThisApp() OtherTabs.Contributor -> ContributorList(onClickContributor) + OtherTabs.Settings -> Settings() OtherTabs.Staff -> StaffList() else -> { val context = LocalContext.current diff --git a/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/setting/Setting.kt b/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/setting/Setting.kt new file mode 100644 index 000000000..05abe3739 --- /dev/null +++ b/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/setting/Setting.kt @@ -0,0 +1,171 @@ +package io.github.droidkaigi.feeder.setting +import android.content.Context +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectable +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.RadioButton +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.github.droidkaigi.feeder.Theme +import io.github.droidkaigi.feeder.core.theme.getTitle +import io.github.droidkaigi.feeder.core.theme.typography +import io.github.droidkaigi.feeder.core.use + +@Preview +@Composable +fun Settings() { + ProvideSettingViewModel(viewModel = settingViewModel()) { + val ( + state, + _, + dispatch, + ) = use(settingViewModel()) + val context = LocalContext.current + + Surface( + color = MaterialTheme.colors.background, + ) { + ThemeSetting( + context = context, + theme = state.theme, + onClick = { + dispatch(SettingViewModel.Event.ChangeTheme(theme = it)) + } + ) + } + } +} + +@Composable +private fun ThemeSetting( + context: Context, + theme: Theme?, + onClick: (Theme?) -> Unit +) { + val openDialog = remember { mutableStateOf(false) } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable { + openDialog.value = true + } + .fillMaxWidth() + .padding(vertical = 24.dp) + ) { + Text( + text = "Theme", + style = typography.h5, + modifier = Modifier.padding(start = 36.dp), + ) + Text( + text = theme.getTitle(context), + style = TextStyle( + color = Color.Gray + ), + modifier = Modifier.fillMaxWidth().padding(end = 36.dp), + textAlign = TextAlign.Right + ) + if (openDialog.value) { + ThemeSelectDialog( + onChangeTheme = onClick, + theme = theme, + context = context, + onClick = { + openDialog.value = false + } + ) + } + } +} + +@Composable +private fun ThemeSelectDialog( + onChangeTheme: (Theme?) -> Unit, + theme: Theme?, + context: Context, + onClick: () -> Unit +) { + AlertDialog( + onDismissRequest = onClick, + title = { + Text(text = "Theme") + }, + text = { + ThemeSelectRadioButton( + onChangeTheme = onChangeTheme, + theme = theme, + context = context + ) + }, + confirmButton = { + Button( + onClick = onClick + ) { + Text("OK") + } + }, + ) +} + +@Composable +private fun ThemeSelectRadioButton( + onChangeTheme: (Theme?) -> Unit, + theme: Theme?, + context: Context +) { + val themes: List = Theme.values().toList() + var defaultIndex = 0 + themes.forEachIndexed { index, it -> if (it == theme) defaultIndex = index } + val (selectedTheme, oThemeSelected) = remember { mutableStateOf(themes[defaultIndex]) } + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column { + themes.forEach { theme -> + Row( + Modifier + .fillMaxWidth() + .selectable( + selected = (theme == selectedTheme), + onClick = { + oThemeSelected(theme) + onChangeTheme(theme) + } + ) + ) { + RadioButton( + selected = (theme == selectedTheme), + modifier = Modifier.padding(16.dp), + onClick = { + oThemeSelected(theme) + onChangeTheme(theme) + } + ) + Text( + text = theme.getTitle(context), + modifier = Modifier.padding(16.dp), + ) + } + } + } + } +} diff --git a/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/setting/SettingViewModel.kt b/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/setting/SettingViewModel.kt new file mode 100644 index 000000000..b93451a11 --- /dev/null +++ b/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/setting/SettingViewModel.kt @@ -0,0 +1,44 @@ +package io.github.droidkaigi.feeder.setting + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf +import io.github.droidkaigi.feeder.AppError +import io.github.droidkaigi.feeder.Theme +import io.github.droidkaigi.feeder.core.UnidirectionalViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +interface SettingViewModel : + UnidirectionalViewModel< + SettingViewModel.Event, + SettingViewModel.Effect, + SettingViewModel.State> { + data class State( + val theme: Theme? = Theme.SYSTEM, + ) + + sealed class Effect { + data class ErrorMessage(val appError: AppError) : Effect() + } + + sealed class Event { + class ChangeTheme(val theme: Theme?) : Event() + } + + override val state: StateFlow + override val effect: Flow + override fun event(event: Event) +} + +private val LocalSettingViewModel = compositionLocalOf { + error("not LocalSettingViewModel provided") +} + +@Composable +fun ProvideSettingViewModel(viewModel: SettingViewModel, block: @Composable () -> Unit) { + CompositionLocalProvider(LocalSettingViewModel provides viewModel, content = block) +} + +@Composable +fun settingViewModel() = LocalSettingViewModel.current