Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dark theme setting for settings screen #339

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -57,10 +58,22 @@ abstract class UserDataStore {
val idToken: StateFlow<String?> = mutableIdToken
suspend fun setIdToken(token: String) = mutableIdToken.emit(token)

fun theme(): Flow<Theme?> {
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"
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Theme?> {
return dataStore.theme()
}
}
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -67,4 +68,11 @@ class DataModule {
): ContributorRepository {
return daggerRepository
}

@Provides
internal fun provideThemeRepository(
daggerRepository: DaggerThemeRepositoryImpl
): ThemeRepository {
return daggerRepository
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.github.droidkaigi.feeder

enum class Theme {
SYSTEM,
DARK,
LIGHT
}
Original file line number Diff line number Diff line change
@@ -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<Theme?>
}
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -22,32 +24,46 @@ 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🆒

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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
3 changes: 3 additions & 0 deletions uicomponent-compose/core/src/main/res/values-ja/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
<string name="error_network">ネットワークエラーが発生しました。もう一度お試しください。</string>
<string name="error_server">サーバーエラーが発生しました。もう一度お試しください。</string>
<string name="error_unknown">予期しないエラーが発生しました。もう一度お試しください。</string>
<string name="system">端末設定に従う</string>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🇯🇵

<string name="dark">ダーク</string>
<string name="light">ライト</string>
</resources>
3 changes: 3 additions & 0 deletions uicomponent-compose/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
<string name="error_network">A network error has occurred.Please try again.</string>
<string name="error_server">A server error has occurred.Please try again.</string>
<string name="error_unknown">An unknown error has occurred.Please try again.</string>
<string name="system">Follow the device settings</string>
<string name="dark">Dark</string>
<string name="light">Light</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
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)
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 DroidKaigiAppViewModel :
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, I don't use the name of DroidKaigi other than the theme, so I want to use AppViewModel.

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<State>
override val effect: Flow<Effect>
override fun event(event: Event)
}

private val LocalDroidKaigiAppViewModel = compositionLocalOf<DroidKaigiAppViewModel> {
error("not LocalDroidKaigiAppViewModel provided")
}

@Composable
fun ProvideDroidKaigiAppViewModel(
viewModel: DroidKaigiAppViewModel,
block: @Composable () -> Unit
) {
CompositionLocalProvider(LocalDroidKaigiAppViewModel provides viewModel, content = block)
}

@Composable
fun droidKaigiAppViewModel() = LocalDroidKaigiAppViewModel.current
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fun LandingScreen(modifier: Modifier = Modifier, onTimeout: () -> Unit) {
@Preview(showBackground = true)
@Composable
fun PreviewLandingScreen() {
ConferenceAppFeederTheme(false) {
ConferenceAppFeederTheme {
LandingScreen(
onTimeout = {}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.ProvideDroidKaigiAppViewModel
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<RealFeedViewModel>()) {
ProvideStaffViewModel(viewModel = fakeStaffViewModel()) {
ProvideContributorViewModel(viewModel = fakeContributorViewModel()) {
content()
ProvideDroidKaigiAppViewModel(viewModel<RealDroidKaigiAppViewModel>()) {
ProvideFeedViewModel(viewModel<RealFeedViewModel>()) {
ProvideSettingViewModel(viewModel<RealSettingViewModel>()) {
ProvideStaffViewModel(viewModel = fakeStaffViewModel()) {
ProvideContributorViewModel(viewModel = fakeContributorViewModel()) {
content()
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.DroidKaigiAppViewModel
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 RealDroidKaigiAppViewModel @Inject constructor(
private val repository: ThemeRepository,
) : ViewModel(), DroidKaigiAppViewModel {
private val effectChannel = Channel<DroidKaigiAppViewModel.Effect>(Channel.UNLIMITED)
override val effect: Flow<DroidKaigiAppViewModel.Effect> = effectChannel.receiveAsFlow()

override val state: StateFlow<DroidKaigiAppViewModel.State> =
repository.theme().map { DroidKaigiAppViewModel.State(theme = it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = DroidKaigiAppViewModel.State()
)

override fun event(event: DroidKaigiAppViewModel.Event) {}
}
Loading