diff --git a/app-android/src/main/java/io/github/droidkaigi/confsched2022/KaigiApp.kt b/app-android/src/main/java/io/github/droidkaigi/confsched2022/KaigiApp.kt index 78f770297..6f94a2546 100644 --- a/app-android/src/main/java/io/github/droidkaigi/confsched2022/KaigiApp.kt +++ b/app-android/src/main/java/io/github/droidkaigi/confsched2022/KaigiApp.kt @@ -44,6 +44,7 @@ import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -54,6 +55,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController @@ -71,6 +73,8 @@ import io.github.droidkaigi.confsched2022.feature.map.MapNavGraph import io.github.droidkaigi.confsched2022.feature.map.mapGraph import io.github.droidkaigi.confsched2022.feature.sessions.SessionsNavGraph import io.github.droidkaigi.confsched2022.feature.sessions.sessionsNavGraph +import io.github.droidkaigi.confsched2022.feature.setting.AppUiModel +import io.github.droidkaigi.confsched2022.feature.setting.KaigiAppViewModel import io.github.droidkaigi.confsched2022.feature.setting.SettingNavGraph import io.github.droidkaigi.confsched2022.feature.setting.settingNavGraph import io.github.droidkaigi.confsched2022.feature.sponsors.SponsorsNavGraph @@ -91,11 +95,14 @@ import kotlinx.coroutines.launch @Composable fun KaigiApp( windowSizeClass: WindowSizeClass, + kaigiAppViewModel: KaigiAppViewModel = hiltViewModel(), kaigiAppScaffoldState: KaigiAppScaffoldState = rememberKaigiAppScaffoldState(), kaigiExternalNavigationController: KaigiExternalNavigationController = rememberKaigiExternalNavigationController(), ) { - KaigiTheme { + val appUiModel: AppUiModel by kaigiAppViewModel.uiModel + + KaigiTheme(isDynamicColorEnabled = appUiModel.isDynamicColorEnabled) { val usePersistentNavigationDrawer = windowSizeClass.usePersistentNavigationDrawer KaigiAppDrawer( kaigiAppScaffoldState = kaigiAppScaffoldState, @@ -152,8 +159,10 @@ fun KaigiApp( onNavigationIconClick = kaigiAppScaffoldState::onNavigationClick, ) settingNavGraph( - showNavigationIcon, - kaigiAppScaffoldState::onNavigationClick + appUiModel = appUiModel, + showNavigationIcon = true, + onDynamicColorToggle = kaigiAppViewModel::onDynamicColorToggle, + onNavigationIconClick = kaigiAppScaffoldState::onNavigationClick ) sponsorsNavGraph( showNavigationIcon = showNavigationIcon, diff --git a/core/data/src/androidMain/kotlin/io/github/droidkaigi/confsched2022/data/setting/di/SettingDataModule.kt b/core/data/src/androidMain/kotlin/io/github/droidkaigi/confsched2022/data/setting/di/SettingDataModule.kt new file mode 100644 index 000000000..5b21d17b2 --- /dev/null +++ b/core/data/src/androidMain/kotlin/io/github/droidkaigi/confsched2022/data/setting/di/SettingDataModule.kt @@ -0,0 +1,24 @@ +package io.github.droidkaigi.confsched2022.data.setting.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import io.github.droidkaigi.confsched2022.data.SettingsDatastore +import io.github.droidkaigi.confsched2022.data.setting.DataDynamicColorSettingRepository +import io.github.droidkaigi.confsched2022.model.DynamicColorSettingRepository +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +public class SettingDataModule { + @Provides + @Singleton + public fun provideSessionsRepository( + settingsDatastore: SettingsDatastore + ): DynamicColorSettingRepository { + return DataDynamicColorSettingRepository( + settingsDatastore = settingsDatastore + ) + } +} diff --git a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/SettingsDatastore.kt b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/SettingsDatastore.kt index 5e606d385..78fb1b8b9 100644 --- a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/SettingsDatastore.kt +++ b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/SettingsDatastore.kt @@ -1,6 +1,7 @@ package io.github.droidkaigi.confsched2022.data import com.russhwolf.settings.coroutines.FlowSettings +import io.github.droidkaigi.confsched2022.model.DroidKaigi2022Day import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.flow.Flow @@ -61,10 +62,26 @@ public class SettingsDatastore(private val flowSettings: FlowSettings) { ) } + public fun dynamicColorEnabled(): Flow { + return flowSettings.getBooleanFlow( + key = KEY_DYNAMIC_COLOR, + // The trick + defaultValue = DroidKaigi2022Day.defaultDyamicThemeDate() + ) + } + + public suspend fun setDynamicColorEnabled(dynamicColorEnabled: Boolean) { + flowSettings.putBoolean( + KEY_DYNAMIC_COLOR, + dynamicColorEnabled, + ) + } + public companion object { public const val NAME: String = "PREFERENCES_NAME" private const val KEY_AUTHENTICATED = "KEY_AUTHENTICATED" private const val KEY_DEVICE_ID = "KEY_DEVICE_ID" + private const val KEY_DYNAMIC_COLOR = "KEY_DYNAMIC_COLOR" private const val KEY = "favorites" private const val DELIMITER = "," } diff --git a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/setting/DataDynamicColorSettingRepository.kt b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/setting/DataDynamicColorSettingRepository.kt new file mode 100644 index 000000000..12860ef64 --- /dev/null +++ b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/setting/DataDynamicColorSettingRepository.kt @@ -0,0 +1,17 @@ +package io.github.droidkaigi.confsched2022.data.setting + +import io.github.droidkaigi.confsched2022.data.SettingsDatastore +import io.github.droidkaigi.confsched2022.model.DynamicColorSettingRepository +import kotlinx.coroutines.flow.Flow + +public class DataDynamicColorSettingRepository( + private val settingsDatastore: SettingsDatastore +) : DynamicColorSettingRepository { + override fun dynamicEnabledFlow(): Flow { + return settingsDatastore.dynamicColorEnabled() + } + + override suspend fun setDynamicColorEnabled(dynamicColorEnabled: Boolean) { + settingsDatastore.setDynamicColorEnabled(dynamicColorEnabled = dynamicColorEnabled) + } +} diff --git a/core/designsystem/src/androidMain/kotlin/io/github/droidkaigi/confsched2022/designsystem/theme/Theme.kt b/core/designsystem/src/androidMain/kotlin/io/github/droidkaigi/confsched2022/designsystem/theme/Theme.kt index a139a3e3e..9916b4de6 100644 --- a/core/designsystem/src/androidMain/kotlin/io/github/droidkaigi/confsched2022/designsystem/theme/Theme.kt +++ b/core/designsystem/src/androidMain/kotlin/io/github/droidkaigi/confsched2022/designsystem/theme/Theme.kt @@ -1,9 +1,12 @@ package io.github.droidkaigi.confsched2022.designsystem.theme +import android.annotation.SuppressLint import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext private val DarkColorPalette = darkColorScheme( primary = Color(KaigiColors.primaryKeyColor80), @@ -31,16 +34,21 @@ private val DarkColorPalette = darkColorScheme( outline = Color(KaigiColors.neutralVariantKeyColor60), ) +@SuppressLint("NewApi") @Composable -fun KaigiTheme( +public fun KaigiTheme( // Currently, we are not supporting light theme // darkTheme: Boolean = isSystemInDarkTheme(), + isDynamicColorEnabled: Boolean = false, content: @Composable () -> Unit ) { + val colorScheme = if (isDynamicColorEnabled) { + dynamicDarkColorScheme(LocalContext.current) + } else { + DarkColorPalette + } + MaterialTheme( - colorScheme = DarkColorPalette, - typography = Typography, - shapes = Shapes, - content = content + colorScheme = colorScheme, typography = Typography, shapes = Shapes, content = content ) } diff --git a/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/model/DroidKaigi2022Day.kt b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/model/DroidKaigi2022Day.kt index ac546b5f5..7eeec0e78 100644 --- a/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/model/DroidKaigi2022Day.kt +++ b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/model/DroidKaigi2022Day.kt @@ -1,5 +1,6 @@ package io.github.droidkaigi.confsched2022.model +import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone @@ -47,6 +48,10 @@ public enum class DroidKaigi2022Day( time in it.start..it.end } } + + public fun defaultDyamicThemeDate(): Boolean { + return Day1.start < Clock.System.now() + } } } diff --git a/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/model/DynamicColorSettingRepository.kt b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/model/DynamicColorSettingRepository.kt new file mode 100644 index 000000000..77efc83f5 --- /dev/null +++ b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/model/DynamicColorSettingRepository.kt @@ -0,0 +1,8 @@ +package io.github.droidkaigi.confsched2022.model + +import kotlinx.coroutines.flow.Flow + +public interface DynamicColorSettingRepository { + public fun dynamicEnabledFlow(): Flow + public suspend fun setDynamicColorEnabled(dynamicColorEnabled: Boolean) +} diff --git a/core/model/src/commonMain/resources/MR/base/strings.xml b/core/model/src/commonMain/resources/MR/base/strings.xml index a499e43d1..1981bd3d0 100644 --- a/core/model/src/commonMain/resources/MR/base/strings.xml +++ b/core/model/src/commonMain/resources/MR/base/strings.xml @@ -54,6 +54,7 @@ 設定 + ダイナミックカラー ダークモード 言語設定 システムのデフォルト diff --git a/core/model/src/commonMain/resources/MR/en/strings.xml b/core/model/src/commonMain/resources/MR/en/strings.xml index 8ac9640fb..ed33d2dc4 100644 --- a/core/model/src/commonMain/resources/MR/en/strings.xml +++ b/core/model/src/commonMain/resources/MR/en/strings.xml @@ -54,6 +54,7 @@ Setting + Dynamic Color Dark Mode Language System Default diff --git a/core/model/src/commonMain/resources/MR/zh/strings.xml b/core/model/src/commonMain/resources/MR/zh/strings.xml index e3094e9cf..9216fc81e 100644 --- a/core/model/src/commonMain/resources/MR/zh/strings.xml +++ b/core/model/src/commonMain/resources/MR/zh/strings.xml @@ -55,6 +55,8 @@ 设置 + + Dynamic Color 深色模式 语言设置 系统默认设置 diff --git a/feature/setting/src/main/java/io/github/droidkaigi/confsched2022/feature/setting/AppUiModel.kt b/feature/setting/src/main/java/io/github/droidkaigi/confsched2022/feature/setting/AppUiModel.kt new file mode 100644 index 000000000..d74c0f011 --- /dev/null +++ b/feature/setting/src/main/java/io/github/droidkaigi/confsched2022/feature/setting/AppUiModel.kt @@ -0,0 +1,5 @@ +package io.github.droidkaigi.confsched2022.feature.setting + +data class AppUiModel( + val isDynamicColorEnabled: Boolean, +) diff --git a/feature/setting/src/main/java/io/github/droidkaigi/confsched2022/feature/setting/KaigiAppViewModel.kt b/feature/setting/src/main/java/io/github/droidkaigi/confsched2022/feature/setting/KaigiAppViewModel.kt new file mode 100644 index 000000000..483864fb0 --- /dev/null +++ b/feature/setting/src/main/java/io/github/droidkaigi/confsched2022/feature/setting/KaigiAppViewModel.kt @@ -0,0 +1,49 @@ +package io.github.droidkaigi.confsched2022.feature.setting + +import android.os.Build +import android.os.Build.VERSION_CODES +import androidx.annotation.ChecksSdkIntAtLeast +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.cash.molecule.AndroidUiDispatcher +import app.cash.molecule.RecompositionClock.ContextClock +import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.droidkaigi.confsched2022.model.DroidKaigi2022Day +import io.github.droidkaigi.confsched2022.model.DynamicColorSettingRepository +import io.github.droidkaigi.confsched2022.ui.moleculeComposeState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class KaigiAppViewModel @Inject constructor( + private val dynamicColorSettingRepository: DynamicColorSettingRepository, +) : ViewModel() { + private val moleculeScope = + CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) + + private val dynamicColorEnabledFlow: Flow = + dynamicColorSettingRepository.dynamicEnabledFlow() + + val uiModel: State = moleculeScope.moleculeComposeState(clock = ContextClock) { + val dynamicColorSettingEnabled by dynamicColorEnabledFlow.collectAsState( + initial = DroidKaigi2022Day.defaultDyamicThemeDate() + ) + AppUiModel(isDynamicColorEnabled = dynamicColorSettingEnabled && isSupportedDynamicColor()) + } + + fun onDynamicColorToggle(isDynamic: Boolean) { + viewModelScope.launch { + dynamicColorSettingRepository.setDynamicColorEnabled(isDynamic) + } + } + + @ChecksSdkIntAtLeast(api = VERSION_CODES.S) + private fun isSupportedDynamicColor(): Boolean { + return Build.VERSION.SDK_INT >= VERSION_CODES.S + } +} diff --git a/feature/setting/src/main/java/io/github/droidkaigi/confsched2022/feature/setting/Setting.kt b/feature/setting/src/main/java/io/github/droidkaigi/confsched2022/feature/setting/Setting.kt index f55d00047..828853cfa 100644 --- a/feature/setting/src/main/java/io/github/droidkaigi/confsched2022/feature/setting/Setting.kt +++ b/feature/setting/src/main/java/io/github/droidkaigi/confsched2022/feature/setting/Setting.kt @@ -1,5 +1,7 @@ package io.github.droidkaigi.confsched2022.feature.setting +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES import androidx.appcompat.app.AppCompatDelegate import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -11,11 +13,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.selection.selectable import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Colorize import androidx.compose.material.icons.filled.Language import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.RadioButton +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf @@ -35,20 +39,26 @@ import io.github.droidkaigi.confsched2022.strings.Strings @Composable fun SettingScreenRoot( + appUiModel: AppUiModel, + onDynamicColorToggle: (Boolean) -> Unit, showNavigationIcon: Boolean = true, onNavigationIconClick: () -> Unit = {} ) { Setting( + appUiModel = appUiModel, showNavigationIcon = showNavigationIcon, - onNavigationIconClick = onNavigationIconClick + onNavigationIconClick = onNavigationIconClick, + onDynamicColorToggle = onDynamicColorToggle, ) } @Composable fun Setting( + appUiModel: AppUiModel, showNavigationIcon: Boolean, + onNavigationIconClick: () -> Unit, + onDynamicColorToggle: (Boolean) -> Unit, modifier: Modifier = Modifier, - onNavigationIconClick: () -> Unit ) { KaigiScaffold( modifier = modifier, @@ -72,6 +82,12 @@ fun Setting( horizontalAlignment = Alignment.Start ) { LanguageSetting() + if (VERSION.SDK_INT >= VERSION_CODES.S) { + DynamicColorSetting( + isDynamicColorEnabled = appUiModel.isDynamicColorEnabled, + onDynamicColorToggle = onDynamicColorToggle, + ) + } } } } @@ -84,9 +100,9 @@ private fun LanguageSetting( Row( modifier = modifier - .padding(vertical = 8.dp) - .fillMaxWidth() - .clickable { openDialog.value = true }, + .clickable { openDialog.value = true } + .padding(16.dp) + .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { @@ -159,11 +175,41 @@ private fun LanguageSelector( } } +@Composable +private fun DynamicColorSetting( + isDynamicColorEnabled: Boolean, + onDynamicColorToggle: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(28.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(imageVector = Icons.Default.Colorize, contentDescription = null) + Text( + text = stringResource(resource = Strings.setting_item_dynamic_color), + modifier = Modifier.weight(1f), + ) + Switch( + checked = isDynamicColorEnabled, + onCheckedChange = { + onDynamicColorToggle(it) + }, + ) + } +} + @Preview @Composable private fun SettingPreview() { KaigiTheme { - SettingScreenRoot() + SettingScreenRoot( + appUiModel = AppUiModel(false), + onDynamicColorToggle = {} + ) } } diff --git a/feature/setting/src/main/java/io/github/droidkaigi/confsched2022/feature/setting/SettingNavGraph.kt b/feature/setting/src/main/java/io/github/droidkaigi/confsched2022/feature/setting/SettingNavGraph.kt index 42170c0ce..5883e7142 100644 --- a/feature/setting/src/main/java/io/github/droidkaigi/confsched2022/feature/setting/SettingNavGraph.kt +++ b/feature/setting/src/main/java/io/github/droidkaigi/confsched2022/feature/setting/SettingNavGraph.kt @@ -4,11 +4,15 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable fun NavGraphBuilder.settingNavGraph( + appUiModel: AppUiModel, showNavigationIcon: Boolean, + onDynamicColorToggle: (Boolean) -> Unit, onNavigationIconClick: () -> Unit ) { composable(route = SettingNavGraph.settingRoute) { SettingScreenRoot( + appUiModel = appUiModel, + onDynamicColorToggle = onDynamicColorToggle, showNavigationIcon = showNavigationIcon, onNavigationIconClick = onNavigationIconClick )