mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
CU-2a1ah8b_Theme-selector (#2178)
App | Feature | Add possibility to change night mode
This commit is contained in:
parent
096811f539
commit
9ff3233747
20 changed files with 703 additions and 10 deletions
|
@ -13,6 +13,7 @@ import com.anytypeio.anytype.di.feature.sets.viewer.ViewerCardSizeSelectModule
|
|||
import com.anytypeio.anytype.di.feature.sets.viewer.ViewerImagePreviewSelectModule
|
||||
import com.anytypeio.anytype.di.feature.settings.AboutAppModule
|
||||
import com.anytypeio.anytype.di.feature.settings.AccountAndDataModule
|
||||
import com.anytypeio.anytype.di.feature.settings.AppearanceModule
|
||||
import com.anytypeio.anytype.di.feature.settings.LogoutWarningModule
|
||||
import com.anytypeio.anytype.di.feature.settings.MainSettingsModule
|
||||
import com.anytypeio.anytype.di.feature.wallpaper.WallpaperSelectModule
|
||||
|
@ -669,6 +670,10 @@ class ComponentManager(private val main: MainComponent) {
|
|||
main.accountAndDataComponent().module(AccountAndDataModule).build()
|
||||
}
|
||||
|
||||
val appearanceComponent = Component {
|
||||
main.appearanceComponent().module(AppearanceModule).build()
|
||||
}
|
||||
|
||||
val logoutWarningComponent = Component {
|
||||
main.logoutWarningComponent().module(LogoutWarningModule).build()
|
||||
}
|
||||
|
|
|
@ -11,10 +11,13 @@ import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
|||
import com.anytypeio.anytype.domain.config.FlavourConfigProvider
|
||||
import com.anytypeio.anytype.domain.config.UserSettingsRepository
|
||||
import com.anytypeio.anytype.domain.device.PathProvider
|
||||
import com.anytypeio.anytype.domain.theme.GetTheme
|
||||
import com.anytypeio.anytype.domain.wallpaper.ObserveWallpaper
|
||||
import com.anytypeio.anytype.domain.wallpaper.RestoreWallpaper
|
||||
import com.anytypeio.anytype.presentation.main.MainViewModelFactory
|
||||
import com.anytypeio.anytype.ui.main.MainActivity
|
||||
import com.anytypeio.anytype.ui_settings.appearance.ThemeApplicator
|
||||
import com.anytypeio.anytype.ui_settings.appearance.ThemeApplicatorImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.Subcomponent
|
||||
|
@ -80,6 +83,18 @@ object MainEntryModule {
|
|||
repo: UserSettingsRepository
|
||||
): RestoreWallpaper = RestoreWallpaper(repo)
|
||||
|
||||
@JvmStatic
|
||||
@PerScreen
|
||||
@Provides
|
||||
fun provideSetupThemeUseCase(
|
||||
repo: UserSettingsRepository
|
||||
): GetTheme = GetTheme(repo)
|
||||
|
||||
@JvmStatic
|
||||
@PerScreen
|
||||
@Provides
|
||||
fun provideThemeApplicator(): ThemeApplicator = ThemeApplicatorImpl()
|
||||
|
||||
@JvmStatic
|
||||
@PerScreen
|
||||
@Provides
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
package com.anytypeio.anytype.di.feature.settings
|
||||
|
||||
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
|
||||
import com.anytypeio.anytype.domain.config.UserSettingsRepository
|
||||
import com.anytypeio.anytype.domain.theme.GetTheme
|
||||
import com.anytypeio.anytype.domain.theme.SetTheme
|
||||
import com.anytypeio.anytype.ui.settings.AppearanceFragment
|
||||
import com.anytypeio.anytype.ui_settings.appearance.AppearanceViewModel
|
||||
import com.anytypeio.anytype.ui_settings.appearance.ThemeApplicator
|
||||
import com.anytypeio.anytype.ui_settings.appearance.ThemeApplicatorImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.Subcomponent
|
||||
|
||||
@Subcomponent(modules = [AppearanceModule::class])
|
||||
@PerScreen
|
||||
interface AppearanceSubComponent {
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
fun module(module: AppearanceModule): Builder
|
||||
fun build(): AppearanceSubComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: AppearanceFragment)
|
||||
}
|
||||
|
||||
@Module
|
||||
object AppearanceModule {
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideViewModelFactory(
|
||||
getTheme: GetTheme,
|
||||
setTheme: SetTheme,
|
||||
themeApplicator: ThemeApplicator
|
||||
): AppearanceViewModel.Factory = AppearanceViewModel.Factory(
|
||||
getTheme,
|
||||
setTheme,
|
||||
themeApplicator
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@PerScreen
|
||||
@Provides
|
||||
fun provideThemeApplicator(): ThemeApplicator = ThemeApplicatorImpl()
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideGetThemeUseCase(repo: UserSettingsRepository): GetTheme = GetTheme(repo)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideSetThemeUseCase(repo: UserSettingsRepository): SetTheme = SetTheme(repo)
|
||||
}
|
|
@ -5,6 +5,7 @@ import com.anytypeio.anytype.di.feature.*
|
|||
import com.anytypeio.anytype.di.feature.auth.DeletedAccountSubcomponent
|
||||
import com.anytypeio.anytype.di.feature.settings.AboutAppSubComponent
|
||||
import com.anytypeio.anytype.di.feature.settings.AccountAndDataSubComponent
|
||||
import com.anytypeio.anytype.di.feature.settings.AppearanceSubComponent
|
||||
import com.anytypeio.anytype.di.feature.settings.LogoutWarningSubComponent
|
||||
import com.anytypeio.anytype.di.feature.settings.MainSettingsSubComponent
|
||||
import com.anytypeio.anytype.di.feature.wallpaper.WallpaperSelectSubComponent
|
||||
|
@ -59,6 +60,7 @@ interface MainComponent {
|
|||
|
||||
fun aboutAppComponent() : AboutAppSubComponent.Builder
|
||||
fun accountAndDataComponent() : AccountAndDataSubComponent.Builder
|
||||
fun appearanceComponent() : AppearanceSubComponent.Builder
|
||||
fun debugSettingsBuilder(): DebugSettingsSubComponent.Builder
|
||||
fun keychainPhraseComponentBuilder(): KeychainPhraseSubComponent.Builder
|
||||
fun otherSettingsComponentBuilder(): OtherSettingsSubComponent.Builder
|
||||
|
|
|
@ -16,8 +16,11 @@ import androidx.navigation.findNavController
|
|||
import com.anytypeio.anytype.BuildConfig
|
||||
import com.anytypeio.anytype.R
|
||||
import com.anytypeio.anytype.app.DefaultAppActionManager
|
||||
import com.anytypeio.anytype.core_models.ThemeMode
|
||||
import com.anytypeio.anytype.core_models.Wallpaper
|
||||
import com.anytypeio.anytype.di.common.componentManager
|
||||
import com.anytypeio.anytype.domain.base.BaseUseCase
|
||||
import com.anytypeio.anytype.domain.theme.GetTheme
|
||||
import com.anytypeio.anytype.navigation.Navigator
|
||||
import com.anytypeio.anytype.presentation.editor.cover.CoverGradient
|
||||
import com.anytypeio.anytype.presentation.main.MainViewModel
|
||||
|
@ -26,7 +29,10 @@ import com.anytypeio.anytype.presentation.main.MainViewModelFactory
|
|||
import com.anytypeio.anytype.presentation.navigation.AppNavigation
|
||||
import com.anytypeio.anytype.presentation.wallpaper.WallpaperColor
|
||||
import com.anytypeio.anytype.ui.editor.CreateObjectFragment
|
||||
import com.anytypeio.anytype.ui_settings.appearance.ThemeApplicator
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Provider {
|
||||
|
@ -41,12 +47,19 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr
|
|||
@Inject
|
||||
lateinit var factory: MainViewModelFactory
|
||||
|
||||
val container : FragmentContainerView get() = findViewById(R.id.fragment)
|
||||
@Inject
|
||||
lateinit var getTheme: GetTheme
|
||||
|
||||
@Inject
|
||||
lateinit var themeApplicator: ThemeApplicator
|
||||
|
||||
val container: FragmentContainerView get() = findViewById(R.id.fragment)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setupWindowInsets()
|
||||
inject()
|
||||
setupTheme()
|
||||
super.onCreate(savedInstanceState)
|
||||
if (savedInstanceState != null) vm.onRestore()
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
|
@ -77,6 +90,19 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr
|
|||
}
|
||||
}
|
||||
|
||||
private fun setupTheme() {
|
||||
runBlocking {
|
||||
getTheme(BaseUseCase.None).proceed(
|
||||
success = {
|
||||
setTheme(it)
|
||||
},
|
||||
failure = {
|
||||
Timber.e(it, "Error while setting current app theme")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
if (intent != null && intent.action == Intent.ACTION_VIEW) {
|
||||
|
@ -94,6 +120,10 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr
|
|||
}
|
||||
}
|
||||
|
||||
private fun setTheme(themeMode: ThemeMode) {
|
||||
themeApplicator.apply(themeMode)
|
||||
}
|
||||
|
||||
private fun setWallpaper(wallpaper: Wallpaper) {
|
||||
when (wallpaper) {
|
||||
is Wallpaper.Gradient -> {
|
||||
|
|
|
@ -56,7 +56,7 @@ class AboutAppFragment : BaseBottomSheetComposeFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getVersionText() : String {
|
||||
private fun getVersionText(): String {
|
||||
val version = BuildConfig.VERSION_NAME
|
||||
return if (version.isNotEmpty()) {
|
||||
if (BuildConfig.DEBUG)
|
||||
|
@ -117,5 +117,15 @@ val typography = Typography(
|
|||
fontFamily = fonts,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 17.sp
|
||||
),
|
||||
body2 = TextStyle(
|
||||
fontFamily = fonts,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 13.sp,
|
||||
),
|
||||
caption = TextStyle(
|
||||
fontFamily = fonts,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 11.sp
|
||||
)
|
||||
)
|
|
@ -5,15 +5,25 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.anytypeio.anytype.R
|
||||
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
|
||||
import com.anytypeio.anytype.di.common.componentManager
|
||||
import com.anytypeio.anytype.ui_settings.appearance.AppearanceScreen
|
||||
import com.anytypeio.anytype.ui_settings.appearance.AppearanceViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
class AppearanceFragment : BaseBottomSheetComposeFragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var factory: AppearanceViewModel.Factory
|
||||
|
||||
private val vm by viewModels<AppearanceViewModel> { factory }
|
||||
|
||||
private val onWallpaperClicked = {
|
||||
findNavController().navigate(R.id.wallpaperSetScreen)
|
||||
}
|
||||
|
@ -28,13 +38,22 @@ class AppearanceFragment : BaseBottomSheetComposeFragment() {
|
|||
setContent {
|
||||
MaterialTheme(typography = typography) {
|
||||
AppearanceScreen(
|
||||
onWallpaperClicked = onWallpaperClicked
|
||||
onWallpaperClicked = onWallpaperClicked,
|
||||
light = { vm.onLight() },
|
||||
dark = { vm.onDark() },
|
||||
system = { vm.onSystem() },
|
||||
selectedMode = vm.selectedTheme.collectAsState().value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun injectDependencies() {}
|
||||
override fun releaseDependencies() {}
|
||||
override fun injectDependencies() {
|
||||
componentManager().appearanceComponent.get().inject(this)
|
||||
}
|
||||
|
||||
override fun releaseDependencies() {
|
||||
componentManager().appearanceComponent.release()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.anytypeio.anytype.core_models
|
||||
|
||||
sealed class ThemeMode {
|
||||
object System : ThemeMode()
|
||||
object Light : ThemeMode()
|
||||
object Night : ThemeMode()
|
||||
}
|
|
@ -105,6 +105,7 @@
|
|||
<color name="default_tag_background">#FEF3C5</color>
|
||||
|
||||
<color name="dark_amber">#E89D00</color>
|
||||
<color name="amber25">#FFEE94</color>
|
||||
|
||||
<color name="default_filter_tag_background_color">#F3F2EC</color>
|
||||
<color name="default_filter_tag_text_color">#6C6A5F</color>
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package com.anytypeio.anytype.data.auth.repo
|
||||
|
||||
import com.anytypeio.anytype.core_models.Wallpaper
|
||||
import com.anytypeio.anytype.core_models.ThemeMode
|
||||
|
||||
interface UserSettingsCache {
|
||||
suspend fun setDefaultObjectType(type: String, name: String)
|
||||
suspend fun getDefaultObjectType(): Pair<String?, String?>
|
||||
suspend fun setWallpaper(wallpaper: Wallpaper)
|
||||
suspend fun getWallpaper() : Wallpaper
|
||||
suspend fun setThemeMode(mode: ThemeMode)
|
||||
suspend fun getThemeMode(): ThemeMode
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.anytypeio.anytype.data.auth.repo
|
||||
|
||||
import com.anytypeio.anytype.core_models.Wallpaper
|
||||
import com.anytypeio.anytype.core_models.ThemeMode
|
||||
import com.anytypeio.anytype.domain.config.UserSettingsRepository
|
||||
|
||||
class UserSettingsDataRepository(private val cache: UserSettingsCache) : UserSettingsRepository {
|
||||
|
@ -16,4 +17,9 @@ class UserSettingsDataRepository(private val cache: UserSettingsCache) : UserSet
|
|||
}
|
||||
|
||||
override suspend fun getDefaultObjectType(): Pair<String?, String?> = cache.getDefaultObjectType()
|
||||
override suspend fun setThemeMode(mode: ThemeMode) {
|
||||
cache.setThemeMode(mode)
|
||||
}
|
||||
|
||||
override suspend fun getThemeMode(): ThemeMode = cache.getThemeMode()
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.anytypeio.anytype.domain.config
|
||||
|
||||
import com.anytypeio.anytype.core_models.ThemeMode
|
||||
import com.anytypeio.anytype.core_models.Wallpaper
|
||||
|
||||
interface UserSettingsRepository {
|
||||
|
@ -7,4 +8,6 @@ interface UserSettingsRepository {
|
|||
suspend fun getWallpaper(): Wallpaper
|
||||
suspend fun setDefaultObjectType(type: String, name: String)
|
||||
suspend fun getDefaultObjectType(): Pair<String?, String?>
|
||||
suspend fun setThemeMode(mode: ThemeMode)
|
||||
suspend fun getThemeMode(): ThemeMode
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.anytypeio.anytype.domain.theme
|
||||
|
||||
import com.anytypeio.anytype.core_models.ThemeMode
|
||||
import com.anytypeio.anytype.domain.base.BaseUseCase
|
||||
import com.anytypeio.anytype.domain.base.Either
|
||||
import com.anytypeio.anytype.domain.config.UserSettingsRepository
|
||||
|
||||
class GetTheme(
|
||||
private val repo: UserSettingsRepository
|
||||
) : BaseUseCase<ThemeMode, BaseUseCase.None>() {
|
||||
override suspend fun run(params: None) = safe {
|
||||
repo.getThemeMode()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.anytypeio.anytype.domain.theme
|
||||
|
||||
import com.anytypeio.anytype.core_models.ThemeMode
|
||||
import com.anytypeio.anytype.domain.base.BaseUseCase
|
||||
import com.anytypeio.anytype.domain.config.UserSettingsRepository
|
||||
|
||||
class SetTheme(
|
||||
private val repo: UserSettingsRepository
|
||||
) : BaseUseCase<Unit, ThemeMode>() {
|
||||
override suspend fun run(params: ThemeMode) = safe {
|
||||
repo.setThemeMode(params)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.anytypeio.anytype.persistence.repo
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.anytypeio.anytype.core_models.ThemeMode
|
||||
import com.anytypeio.anytype.core_models.Wallpaper
|
||||
import com.anytypeio.anytype.data.auth.repo.UserSettingsCache
|
||||
|
||||
|
@ -78,6 +79,48 @@ class DefaultUserSettingsCache(private val prefs: SharedPreferences) : UserSetti
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun setThemeMode(mode: ThemeMode) {
|
||||
prefs
|
||||
.edit()
|
||||
.putInt(THEME_KEY, mode.toInt())
|
||||
.apply()
|
||||
}
|
||||
|
||||
override suspend fun getThemeMode(): ThemeMode {
|
||||
return if (prefs.contains(THEME_KEY)) {
|
||||
prefs.getInt(THEME_KEY, -1).toTheme()
|
||||
} else {
|
||||
ThemeMode.System
|
||||
}
|
||||
}
|
||||
|
||||
private fun ThemeMode.toInt() = when (this) {
|
||||
ThemeMode.Light -> {
|
||||
THEME_TYPE_LIGHT
|
||||
}
|
||||
ThemeMode.Night -> {
|
||||
THEME_TYPE_NIGHT
|
||||
}
|
||||
ThemeMode.System -> {
|
||||
THEME_TYPE_SYSTEM
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.toTheme() = when (this) {
|
||||
THEME_TYPE_LIGHT -> {
|
||||
ThemeMode.Light
|
||||
}
|
||||
THEME_TYPE_NIGHT -> {
|
||||
ThemeMode.Night
|
||||
}
|
||||
THEME_TYPE_SYSTEM -> {
|
||||
ThemeMode.System
|
||||
}
|
||||
else -> {
|
||||
throw IllegalStateException("Illegal theme key!")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_OBJECT_TYPE_ID_KEY = "prefs.user_settings.default_object_type.id"
|
||||
const val DEFAULT_OBJECT_TYPE_NAME_KEY = "prefs.user_settings.default_object_type.name"
|
||||
|
@ -88,5 +131,10 @@ class DefaultUserSettingsCache(private val prefs: SharedPreferences) : UserSetti
|
|||
|
||||
const val WALLPAPER_TYPE_KEY = "prefs.user_settings.wallpaper_type"
|
||||
const val WALLPAPER_VALUE_KEY = "prefs.user_settings.wallpaper_value"
|
||||
|
||||
const val THEME_KEY = "prefs.user_settings.theme_mode"
|
||||
const val THEME_TYPE_SYSTEM = 1
|
||||
const val THEME_TYPE_LIGHT = 2
|
||||
const val THEME_TYPE_NIGHT = 3
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ dependencies {
|
|||
implementation project(':domain')
|
||||
implementation project(':core-ui')
|
||||
implementation project(':analytics')
|
||||
implementation project(':core-models')
|
||||
|
||||
def applicationDependencies = rootProject.ext.mainApplication
|
||||
def unitTestDependencies = rootProject.ext.unitTesting
|
||||
|
@ -39,6 +40,7 @@ dependencies {
|
|||
implementation applicationDependencies.lifecycleViewModel
|
||||
implementation applicationDependencies.lifecycleRuntime
|
||||
|
||||
implementation applicationDependencies.appcompat
|
||||
implementation applicationDependencies.compose
|
||||
implementation applicationDependencies.composeFoundation
|
||||
implementation applicationDependencies.composeMaterial
|
||||
|
|
|
@ -1,26 +1,73 @@
|
|||
package com.anytypeio.anytype.ui_settings.appearance
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement.SpaceEvenly
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Outline
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawOutline
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_models.ThemeMode
|
||||
import com.anytypeio.anytype.core_ui.foundation.Divider
|
||||
import com.anytypeio.anytype.core_ui.foundation.Dragger
|
||||
import com.anytypeio.anytype.core_ui.foundation.Option
|
||||
import com.anytypeio.anytype.core_ui.foundation.Toolbar
|
||||
import com.anytypeio.anytype.ui_settings.R
|
||||
|
||||
|
||||
private val buttonSize = 60.dp
|
||||
private const val firstQuarterFactor = 0.5f
|
||||
private const val thirdQuartersFactor = 1.5f
|
||||
|
||||
@Composable
|
||||
fun AppearanceScreen(
|
||||
onWallpaperClicked: () -> Unit
|
||||
onWallpaperClicked: () -> Unit,
|
||||
light: () -> Unit,
|
||||
dark: () -> Unit,
|
||||
system: () -> Unit,
|
||||
selectedMode: ThemeMode
|
||||
) {
|
||||
Column {
|
||||
Box(Modifier.padding(vertical = 6.dp).align(Alignment.CenterHorizontally)) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(vertical = 6.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
Dragger()
|
||||
}
|
||||
Toolbar(stringResource(R.string.appearance))
|
||||
|
@ -30,6 +77,304 @@ fun AppearanceScreen(
|
|||
onClick = onWallpaperClicked
|
||||
)
|
||||
Divider(paddingStart = 60.dp)
|
||||
Box(Modifier.height(54.dp))
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 18.dp, bottom = 12.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResource(R.string.mode),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = colorResource(com.anytypeio.anytype.core_ui.R.color.text_secondary),
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 20.dp),
|
||||
horizontalArrangement = SpaceEvenly
|
||||
) {
|
||||
|
||||
LightModeButton(
|
||||
onClick = light,
|
||||
selectedMode == ThemeMode.Light
|
||||
)
|
||||
DarkModeButton(
|
||||
onClick = dark,
|
||||
selectedMode == ThemeMode.Night
|
||||
)
|
||||
SystemModeButton(
|
||||
onClick = system,
|
||||
selectedMode == ThemeMode.System
|
||||
)
|
||||
}
|
||||
Divider(paddingStart = 20.dp)
|
||||
Box(Modifier.height(48.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LightModeButton(
|
||||
onClick: () -> Unit = {},
|
||||
isSelected: Boolean
|
||||
) {
|
||||
ButtonColumn(onClick = onClick) {
|
||||
SelectionBox(isSelected = isSelected) {
|
||||
Text(
|
||||
text = "Aa",
|
||||
style = MaterialTheme.typography.h1,
|
||||
modifier = Modifier
|
||||
.size(buttonSize)
|
||||
.background(
|
||||
color = Color.White,
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = colorResource(id = R.color.shape_primary),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
)
|
||||
.wrapContentSize(align = Alignment.Center)
|
||||
)
|
||||
}
|
||||
ModeNameText(id = R.string.light)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DarkModeButton(
|
||||
onClick: () -> Unit = {},
|
||||
isSelected: Boolean
|
||||
) {
|
||||
ButtonColumn(onClick = onClick) {
|
||||
SelectionBox(isSelected = isSelected) {
|
||||
Text(
|
||||
text = "Aa",
|
||||
style = MaterialTheme.typography.h1,
|
||||
color = colorResource(id = R.color.text_white),
|
||||
modifier = Modifier
|
||||
.size(buttonSize)
|
||||
.background(
|
||||
color = Color.Black,
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
)
|
||||
.wrapContentSize(align = Alignment.Center)
|
||||
)
|
||||
}
|
||||
ModeNameText(id = R.string.dark)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SystemModeButton(
|
||||
onClick: () -> Unit = {},
|
||||
isSelected: Boolean
|
||||
) {
|
||||
ButtonColumn(onClick = onClick) {
|
||||
SelectionBox(isSelected = isSelected) {
|
||||
|
||||
val cornersRadius = with(LocalDensity.current) { 14.dp.toPx() }
|
||||
val greyCornerRadius = with(LocalDensity.current) { 15.dp.toPx() }
|
||||
val greyBorderSize = with(LocalDensity.current) { 1.dp.toPx() }
|
||||
val greyColor = colorResource(id = R.color.shape_primary)
|
||||
|
||||
Canvas(
|
||||
modifier = Modifier.size(buttonSize)
|
||||
) {
|
||||
|
||||
val rect = Rect(Offset.Zero, size)
|
||||
|
||||
drawWholeViewGreyRoundedRectangle(this, rect, greyColor, greyCornerRadius)
|
||||
drawFirstQuarterWhiteRoundedRectangle(this, rect, cornersRadius, greyBorderSize)
|
||||
drawSecondQuarterWhiteRectangle(this, rect, greyBorderSize)
|
||||
|
||||
drawThirdQuarterBlackRectangle(this, rect)
|
||||
drawFourthQuarterBlackRoundedRectangle(this, rect, cornersRadius)
|
||||
|
||||
}
|
||||
val annotatedString = buildAnnotatedString {
|
||||
withStyle(style = SpanStyle(Color.Black)) {
|
||||
append("A")
|
||||
}
|
||||
withStyle(style = SpanStyle(Color.White)) {
|
||||
append("a")
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = annotatedString,
|
||||
style = MaterialTheme.typography.h1,
|
||||
modifier = Modifier
|
||||
.size(buttonSize)
|
||||
.wrapContentSize(align = Alignment.Center)
|
||||
)
|
||||
}
|
||||
ModeNameText(id = R.string.system)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectionBox(
|
||||
isSelected: Boolean,
|
||||
content: @Composable BoxScope.() -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.let {
|
||||
if (isSelected) it.border(
|
||||
width = 2.dp,
|
||||
color = colorResource(id = R.color.amber25),
|
||||
shape = RoundedCornerShape(18.dp)
|
||||
) else {
|
||||
it
|
||||
}
|
||||
}
|
||||
.padding(4.dp),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ButtonColumn(
|
||||
onClick: () -> Unit,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
.clickable(onClick = onClick),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModeNameText(
|
||||
@StringRes id: Int
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = id),
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = colorResource(com.anytypeio.anytype.core_ui.R.color.text_secondary),
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview()
|
||||
@Composable
|
||||
fun ComposablePreview() {
|
||||
AppearanceScreen({}, {}, {}, {}, ThemeMode.Light)
|
||||
}
|
||||
|
||||
fun drawThirdQuarterBlackRectangle(
|
||||
drawScope: DrawScope,
|
||||
rect: Rect
|
||||
) {
|
||||
with(drawScope) {
|
||||
val path = Path().apply {
|
||||
moveTo(rect.topCenter.x, rect.topCenter.y)
|
||||
lineTo(rect.topCenter.x * thirdQuartersFactor, rect.topCenter.y)
|
||||
lineTo(rect.bottomCenter.x * thirdQuartersFactor, rect.bottomCenter.y)
|
||||
lineTo(rect.bottomCenter.x, rect.bottomCenter.y)
|
||||
close()
|
||||
}
|
||||
|
||||
drawPathIntoCanvas(this, path, Color.Black)
|
||||
}
|
||||
}
|
||||
|
||||
fun drawFourthQuarterBlackRoundedRectangle(
|
||||
drawScope: DrawScope,
|
||||
rect: Rect,
|
||||
cornersRadius: Float
|
||||
) {
|
||||
with(drawScope) {
|
||||
val path = Path().apply {
|
||||
moveTo(rect.topCenter.x * thirdQuartersFactor - cornersRadius, rect.topCenter.y)
|
||||
lineTo(rect.topRight)
|
||||
lineTo(rect.bottomRight)
|
||||
lineTo(rect.bottomCenter.x * thirdQuartersFactor - cornersRadius, rect.bottomCenter.y)
|
||||
close()
|
||||
}
|
||||
drawPathIntoCanvas(this, path, Color.Black, PathEffect.cornerPathEffect(cornersRadius))
|
||||
}
|
||||
}
|
||||
|
||||
fun drawSecondQuarterWhiteRectangle(
|
||||
drawScope: DrawScope,
|
||||
rect: Rect,
|
||||
greyBorderSize: Float
|
||||
) {
|
||||
with(drawScope) {
|
||||
val path = Path().apply {
|
||||
moveTo(rect.topCenter.x, rect.topCenter.y + greyBorderSize)
|
||||
lineTo(rect.topCenter.x * firstQuarterFactor, rect.topCenter.y + greyBorderSize)
|
||||
lineTo(rect.bottomCenter.x * firstQuarterFactor, rect.bottomCenter.y - greyBorderSize)
|
||||
lineTo(rect.bottomCenter.x, rect.bottomCenter.y - greyBorderSize)
|
||||
close()
|
||||
}
|
||||
drawPathIntoCanvas(this, path, Color.White, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun drawFirstQuarterWhiteRoundedRectangle(
|
||||
drawScope: DrawScope,
|
||||
rect: Rect,
|
||||
cornersRadius: Float,
|
||||
greyBorderSize: Float
|
||||
) {
|
||||
with(drawScope) {
|
||||
val path = Path().apply {
|
||||
moveTo(
|
||||
rect.topCenter.x * firstQuarterFactor + cornersRadius,
|
||||
rect.topCenter.y + greyBorderSize
|
||||
)
|
||||
lineTo(rect.topLeft.x + greyBorderSize, rect.topLeft.y + greyBorderSize)
|
||||
lineTo(rect.bottomLeft.x + greyBorderSize, rect.bottomLeft.y - greyBorderSize)
|
||||
lineTo(
|
||||
rect.bottomCenter.x * firstQuarterFactor + cornersRadius,
|
||||
rect.bottomCenter.y - greyBorderSize
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
drawPathIntoCanvas(this, path, Color.White, PathEffect.cornerPathEffect(cornersRadius))
|
||||
}
|
||||
}
|
||||
|
||||
fun drawWholeViewGreyRoundedRectangle(
|
||||
drawScope: DrawScope,
|
||||
rect: Rect,
|
||||
greyColor: Color,
|
||||
greyCornerRadius: Float
|
||||
) {
|
||||
with(drawScope) {
|
||||
val path = Path().apply {
|
||||
moveTo(rect.topLeft.x, rect.topLeft.y)
|
||||
lineTo(rect.topRight.x, rect.topRight.y)
|
||||
lineTo(rect.bottomRight.x, rect.bottomRight.y)
|
||||
lineTo(rect.bottomLeft.x, rect.bottomLeft.y)
|
||||
close()
|
||||
}
|
||||
|
||||
drawPathIntoCanvas(this, path, greyColor, PathEffect.cornerPathEffect(greyCornerRadius))
|
||||
}
|
||||
}
|
||||
|
||||
fun drawPathIntoCanvas(
|
||||
drawScope: DrawScope,
|
||||
path: Path,
|
||||
toColor: Color,
|
||||
toPathEffect: PathEffect? = null
|
||||
) {
|
||||
with(drawScope) {
|
||||
drawIntoCanvas { canvas ->
|
||||
canvas.drawOutline(
|
||||
outline = Outline.Generic(path),
|
||||
paint = Paint().apply {
|
||||
color = toColor
|
||||
pathEffect = toPathEffect
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Path.lineTo(offset: Offset) = lineTo(offset.x, offset.y)
|
|
@ -0,0 +1,82 @@
|
|||
package com.anytypeio.anytype.ui_settings.appearance
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.anytypeio.anytype.core_models.ThemeMode
|
||||
import com.anytypeio.anytype.domain.base.BaseUseCase
|
||||
import com.anytypeio.anytype.domain.theme.GetTheme
|
||||
import com.anytypeio.anytype.domain.theme.SetTheme
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
class AppearanceViewModel(
|
||||
private val getTheme: GetTheme,
|
||||
private val setTheme: SetTheme,
|
||||
private val themeApplicator: ThemeApplicator,
|
||||
) : ViewModel() {
|
||||
|
||||
val selectedTheme = MutableStateFlow<ThemeMode>(ThemeMode.System)
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
getTheme(BaseUseCase.None).proceed(
|
||||
success = {
|
||||
proceedWithUpdatingSelectedTheme(it)
|
||||
},
|
||||
failure = {
|
||||
Timber.e(it, "Error while getting current app theme")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveTheme(mode: ThemeMode) {
|
||||
viewModelScope.launch {
|
||||
setTheme(params = mode).proceed(
|
||||
success = {
|
||||
proceedWithUpdatingTheme(mode)
|
||||
},
|
||||
failure = {
|
||||
Timber.e(it, "Error while setting current app theme")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onLight() {
|
||||
saveTheme(ThemeMode.Light)
|
||||
}
|
||||
|
||||
fun onDark() {
|
||||
saveTheme(ThemeMode.Night)
|
||||
}
|
||||
|
||||
fun onSystem() {
|
||||
saveTheme(ThemeMode.System)
|
||||
}
|
||||
|
||||
private fun proceedWithUpdatingTheme(themeMode: ThemeMode) {
|
||||
themeApplicator.apply(themeMode)
|
||||
proceedWithUpdatingSelectedTheme(themeMode)
|
||||
}
|
||||
|
||||
private fun proceedWithUpdatingSelectedTheme(themeMode: ThemeMode) {
|
||||
selectedTheme.value = themeMode
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val getTheme: GetTheme,
|
||||
private val setTheme: SetTheme,
|
||||
private val themeApplicator: ThemeApplicator,
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return AppearanceViewModel(
|
||||
getTheme = getTheme,
|
||||
setTheme = setTheme,
|
||||
themeApplicator = themeApplicator
|
||||
) as T
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.anytypeio.anytype.ui_settings.appearance
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import com.anytypeio.anytype.core_models.ThemeMode
|
||||
|
||||
interface ThemeApplicator {
|
||||
|
||||
fun apply(theme: ThemeMode)
|
||||
|
||||
}
|
||||
|
||||
class ThemeApplicatorImpl: ThemeApplicator {
|
||||
override fun apply(theme: ThemeMode) {
|
||||
when(theme) {
|
||||
ThemeMode.Light -> apply(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
ThemeMode.Night -> apply(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
ThemeMode.System -> apply(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
}
|
||||
|
||||
private fun apply(mode: Int) {
|
||||
if (AppCompatDelegate.getDefaultNightMode() != mode) {
|
||||
AppCompatDelegate.setDefaultNightMode(mode)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,11 +14,16 @@
|
|||
<string name="reset_account">Reset account</string>
|
||||
<string name="delete_account">Delete account</string>
|
||||
<string name="data">Data</string>
|
||||
<string name="mode">Mode</string>
|
||||
<string name="log_out">Logout</string>
|
||||
<string name="pin_code">Pin code</string>
|
||||
<string name="access">Access</string>
|
||||
<string name="wallpaper">Wallpaper</string>
|
||||
|
||||
<string name="light">Light</string>
|
||||
<string name="dark">Dark</string>
|
||||
<string name="system">System</string>
|
||||
|
||||
<string name="have_you_back_up_your_keychain">Have you backed up your keychain phrase?</string>
|
||||
<string name="you_will_need_to_sign_in">You will need it to sign in. Keep it in a safe place. If you lose it, you can no longer access your account.</string>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue