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..874ab4806 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 @@ -1,8 +1,10 @@ package io.github.droidkaigi.feeder.data +import com.russhwolf.settings.ExperimentalSettingsApi 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 +59,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..672a4c9b0 --- /dev/null +++ b/data/repository/src/main/java/io/github/droidkaigi/feeder/data/DaggerThemeRepositoryImpl.kt @@ -0,0 +1,12 @@ +package io.github.droidkaigi.feeder.data + +import io.github.droidkaigi.feeder.repository.ThemeRepository +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 738b5ea23..2a95101ac 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 @@ -6,6 +6,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import io.github.droidkaigi.feeder.repository.DeviceRepository import io.github.droidkaigi.feeder.repository.FeedRepository +import io.github.droidkaigi.feeder.repository.ThemeRepository @InstallIn(SingletonComponent::class) @Module @@ -36,4 +37,11 @@ class DataModule { ): DeviceRepository { 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..0d2e0c2b2 --- /dev/null +++ b/model/src/commonMain/kotlin/io/github/droidkaigi/feeder/Theme.kt @@ -0,0 +1,8 @@ +package io.github.droidkaigi.feeder + +enum class Theme { + SYSTEM, + BATTERY, + 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..c869a2a8c --- /dev/null +++ b/model/src/commonMain/kotlin/io/github/droidkaigi/feeder/repository/ThemeRepository.kt @@ -0,0 +1,12 @@ +package io.github.droidkaigi.feeder.repository + +import io.github.droidkaigi.feeder.FeedContents +import io.github.droidkaigi.feeder.FeedItem +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/AppErrorMessage.kt b/uicomponent-compose/core/src/main/java/io/github/droidkaigi/feeder/core/AppErrorMessage.kt index 43a9c087e..ac3ebf69d 100644 --- a/uicomponent-compose/core/src/main/java/io/github/droidkaigi/feeder/core/AppErrorMessage.kt +++ b/uicomponent-compose/core/src/main/java/io/github/droidkaigi/feeder/core/AppErrorMessage.kt @@ -2,6 +2,7 @@ package io.github.droidkaigi.feeder.core import android.content.Context import io.github.droidkaigi.feeder.AppError +import io.github.droidkaigi.feeder.Theme fun AppError.getReadableMessage(context: Context): String = when (this) { is AppError.ApiException.ServerException -> { 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 7f0ab269c..d930f8e14 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,11 +1,13 @@ 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.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, @@ -21,21 +23,36 @@ 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 ) } } + +@Composable +private fun colorPalette(theme: Theme?): Colors { + return when (theme) { + Theme.SYSTEM -> systemColorPalette() + Theme.BATTERY -> DarkColorPalette + Theme.DARK -> DarkColorPalette + Theme.LIGHT -> LightColorPalette + else -> throw IllegalArgumentException("should not happen") + } +} + +@Composable +private fun systemColorPalette(): Colors { + return if (isSystemInDarkTheme()) { + DarkColorPalette + } else { + LightColorPalette + } +} 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..ba968c677 --- /dev/null +++ b/uicomponent-compose/core/src/main/java/io/github/droidkaigi/feeder/core/theme/ThemeTitle.kt @@ -0,0 +1,13 @@ +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.BATTERY -> context.getString(R.string.battery) + Theme.DARK -> context.getString(R.string.dark) + Theme.LIGHT -> context.getString(R.string.light) + else -> context.getString(R.string.error_unknown) +} 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..65dbb7333 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,8 @@ ネットワークエラーが発生しました。もう一度お試しください。 サーバーエラーが発生しました。もう一度お試しください。 予期しないエラーが発生しました。もう一度お試しください。 + 端末設定に従う + 省電力モードの時のみ + ダーク + ライト diff --git a/uicomponent-compose/core/src/main/res/values/strings.xml b/uicomponent-compose/core/src/main/res/values/strings.xml index cfecafba1..dc5259ae5 100644 --- a/uicomponent-compose/core/src/main/res/values/strings.xml +++ b/uicomponent-compose/core/src/main/res/values/strings.xml @@ -3,4 +3,8 @@ A network error has occurred.Please try again. A server error has occurred.Please try again. An unknown error has occurred.Please try again. + system + battery + dark + light diff --git a/uicomponent-compose/feed/src/main/java/io/github/droidkaigi/feeder/feed/FeedScreen.kt b/uicomponent-compose/feed/src/main/java/io/github/droidkaigi/feeder/feed/FeedScreen.kt index e0bedd943..b0f64f448 100644 --- a/uicomponent-compose/feed/src/main/java/io/github/droidkaigi/feeder/feed/FeedScreen.kt +++ b/uicomponent-compose/feed/src/main/java/io/github/droidkaigi/feeder/feed/FeedScreen.kt @@ -39,6 +39,7 @@ import dev.chrisbanes.accompanist.insets.toPaddingValues import io.github.droidkaigi.feeder.FeedContents import io.github.droidkaigi.feeder.FeedItem import io.github.droidkaigi.feeder.Filters +import io.github.droidkaigi.feeder.Theme import io.github.droidkaigi.feeder.core.getReadableMessage import io.github.droidkaigi.feeder.core.theme.ConferenceAppFeederTheme import io.github.droidkaigi.feeder.core.use @@ -288,7 +289,7 @@ fun FeedItemRow( @Preview(showBackground = true) @Composable fun PreviewFeedScreen() { - ConferenceAppFeederTheme(false) { + ConferenceAppFeederTheme(Theme.LIGHT) { ProvideFeedViewModel(viewModel = fakeFeedViewModel()) { FeedScreen( selectedTab = FeedTabs.Home, @@ -304,7 +305,7 @@ fun PreviewFeedScreen() { @Preview(showBackground = true) @Composable fun PreviewFeedScreenWithStartBlog() { - ConferenceAppFeederTheme(false) { + ConferenceAppFeederTheme(Theme.LIGHT) { ProvideFeedViewModel(viewModel = fakeFeedViewModel()) { FeedScreen( selectedTab = FeedTabs.FilteredFeed.Blog, 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..dfdb6f435 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 @@ -11,39 +11,46 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.lifecycle.viewmodel.compose.viewModel import dev.chrisbanes.accompanist.insets.navigationBarsPadding import io.github.droidkaigi.feeder.core.theme.ConferenceAppFeederTheme +import io.github.droidkaigi.feeder.core.use +import io.github.droidkaigi.feeder.viewmodel.RealDroidKaigiAppViewModel @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 - } + ProvideDroidKaigiAppViewModel(viewModel = droidKaigiAppViewModel()) { + val (state) = use(droidKaigiAppViewModel()) + + 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/DroidKaigiAppViewModel.kt b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/DroidKaigiAppViewModel.kt new file mode 100644 index 000000000..a7028ad28 --- /dev/null +++ b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/DroidKaigiAppViewModel.kt @@ -0,0 +1,42 @@ +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 DroidKaigiAppViewModel : + UnidirectionalViewModel< + DroidKaigiAppViewModel.Event, + DroidKaigiAppViewModel.Effect, + DroidKaigiAppViewModel.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 LocalDroidKaigiAppViewModel = compositionLocalOf { + error("not LocalDroidKaigiAppViewModel provided") +} + +@Composable +fun ProvideDroidKaigiAppViewModel(viewModel: DroidKaigiAppViewModel, block: @Composable () -> +Unit) { + CompositionLocalProvider(LocalDroidKaigiAppViewModel provides viewModel, content = block) +} + +@Composable +fun droidKaigiAppViewModel() = LocalDroidKaigiAppViewModel.current 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 1d25919cb..00bf58821 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,15 +2,21 @@ package io.github.droidkaigi.feeder.viewmodel import androidx.compose.runtime.Composable import androidx.lifecycle.viewmodel.compose.viewModel +import io.github.droidkaigi.feeder.ProvideDroidKaigiAppViewModel 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()) { - content() + ProvideDroidKaigiAppViewModel(viewModel()) { + ProvideFeedViewModel(viewModel()) { + ProvideSettingViewModel(viewModel()) { + ProvideStaffViewModel(viewModel = fakeStaffViewModel()) { + content() + } + } } } } diff --git a/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/viewmodel/RealDroidKaigiAppViewModel.kt b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/viewmodel/RealDroidKaigiAppViewModel.kt new file mode 100644 index 000000000..42a8ccb29 --- /dev/null +++ b/uicomponent-compose/main/src/main/java/io/github/droidkaigi/feeder/viewmodel/RealDroidKaigiAppViewModel.kt @@ -0,0 +1,39 @@ +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.DroidKaigiAppViewModel +import io.github.droidkaigi.feeder.repository.ThemeRepository +import io.github.droidkaigi.feeder.setting.SettingViewModel +import io.github.droidkaigi.feeder.staff.StaffViewModel +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 +import javax.annotation.meta.Exhaustive +import javax.inject.Inject + +@HiltViewModel +class RealDroidKaigiAppViewModel @Inject constructor( + private val repository: ThemeRepository, +) : ViewModel(), DroidKaigiAppViewModel { + private val effectChannel = Channel(Channel.UNLIMITED) + override val effect: Flow = effectChannel.receiveAsFlow() + + override val state: StateFlow = + repository.theme().map { DroidKaigiAppViewModel.State(theme = it)} + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = DroidKaigiAppViewModel.State() + ) + + override fun event(event: DroidKaigiAppViewModel.Event) { + TODO("Not yet implemented") + } +} 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..59aaf4066 --- /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 dagger.hilt.android.lifecycle.HiltViewModel +import io.github.droidkaigi.feeder.setting.SettingViewModel +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 +import kotlinx.coroutines.launch +import javax.annotation.meta.Exhaustive + +@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/other/OtherScreen.kt b/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/other/OtherScreen.kt index f7f2f823f..881902490 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 @@ -36,6 +36,7 @@ import dev.chrisbanes.accompanist.insets.LocalWindowInsets import dev.chrisbanes.accompanist.insets.statusBarsPadding import io.github.droidkaigi.feeder.about.AboutThisApp 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) { @@ -162,6 +163,7 @@ private fun BackdropFrontLayerContent( ) { when (selectedTab) { OtherTabs.AboutThisApp -> AboutThisApp() + 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..2673a1454 --- /dev/null +++ b/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/setting/Setting.kt @@ -0,0 +1,169 @@ +package io.github.droidkaigi.feeder.setting +import android.content.Context +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.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.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.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +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, + effectFlow, + dispatch, + ) = use(settingViewModel()) + + val context = LocalContext.current + Column( + modifier = Modifier + .padding(vertical = 48.dp, horizontal = 20.dp) + ) { + AboutThisAppComponent( + context = context, + theme = state.theme, + onChangeTheme = { + dispatch(SettingViewModel.Event.ChangeTheme(theme = it)) + } + ) + Spacer(modifier = Modifier.height(68.dp)) + } + } +} + + +@Composable +fun AboutThisAppComponent( + context: Context, + theme: Theme?, + onChangeTheme: (Theme?) -> Unit +) { + Column { + if (theme != null) { + Text( + text = theme.getTitle(context), + style = typography.h6 + ) + } + Spacer(modifier = Modifier.height(16.dp)) + AlertDialog( + onChangeTheme = onChangeTheme, + theme = theme, + context = context, + ) + } +} + +@Composable +fun AlertDialog( + onChangeTheme: (Theme?) -> Unit, + theme: Theme?, + context: Context +) { + MaterialTheme { + Column { + val openDialog = remember { mutableStateOf(false) } + + Button(onClick = { + openDialog.value = true + }) { + Text("Change theme") + } + + if (openDialog.value) { + + AlertDialog( + onDismissRequest = { + openDialog.value = false + }, + title = { + Text(text = "Theme") + }, + text = { + SimpleRadioButtonComponent( + onChangeTheme = onChangeTheme, + theme = theme, + context = context + ) + }, + confirmButton = { + Button( + onClick = { + openDialog.value = false + }) { + Text("OK") + } + }, + ) + } + } + + } +} + +@Composable +fun SimpleRadioButtonComponent( + onChangeTheme: (Theme?) -> Unit, + theme: Theme?, + context: Context +) { + val radioOptions: Array = Theme.values() + val (selectedOption, onOptionSelected) = remember { mutableStateOf(theme?.name ?: Theme + .SYSTEM) } + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column { + radioOptions.forEach { text -> + Row( + Modifier + .fillMaxWidth() + .selectable( + selected = (text == selectedOption), + onClick = { + onOptionSelected(text) + onChangeTheme(text) + } + ) + ) { + RadioButton( + selected = (text == selectedOption), + modifier = Modifier.padding(all = Dp(value = 8F)), + onClick = { + onOptionSelected(text) + onChangeTheme(text) + } + ) + Text( + text = text.getTitle(context), + modifier = Modifier.padding(all = Dp(value = 8F)), + ) + } + } + } + } +} 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..fe994a9ef --- /dev/null +++ b/uicomponent-compose/other/src/main/java/io/github/droidkaigi/feeder/setting/SettingViewModel.kt @@ -0,0 +1,41 @@ +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 { + 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