Skip to content

Commit

Permalink
support user profile color
Browse files Browse the repository at this point in the history
  • Loading branch information
DatL4g committed Apr 28, 2024
1 parent 2d88038 commit 765056a
Show file tree
Hide file tree
Showing 13 changed files with 333 additions and 21 deletions.
7 changes: 4 additions & 3 deletions anilist/src/commonMain/graphql/ViewerMutation.graphql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mutation ViewerMutation($adult: Boolean) {
UpdateUser(displayAdultContent: $adult) {
mutation ViewerMutation($adult: Boolean, $color: String) {
UpdateUser(displayAdultContent: $adult, profileColor: $color) {
id,
name,
avatar {
Expand All @@ -8,7 +8,8 @@ mutation ViewerMutation($adult: Boolean) {
},
bannerImage,
options {
displayAdultContent
displayAdultContent,
profileColor
}
}
}
3 changes: 2 additions & 1 deletion anilist/src/commonMain/graphql/ViewerQuery.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ query ViewerQuery {
},
bannerImage,
options {
displayAdultContent
displayAdultContent,
profileColor
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,25 @@ data class User(
val name: String,
val avatar: Avatar = Avatar(),
val banner: String? = null,
val displayAdultContent: Boolean = false
val displayAdultContent: Boolean = false,
val profileColor: String? = null
) {
constructor(query: ViewerQuery.Viewer) : this(
id = query.id,
name = query.name,
avatar = query.avatar.let(::Avatar),
banner = query.bannerImage?.ifBlank { null },
displayAdultContent = query.options?.displayAdultContent ?: false
displayAdultContent = query.options?.displayAdultContent ?: false,
profileColor = query.options?.profileColor?.ifBlank { null }
)

constructor(mutation: ViewerMutation.UpdateUser) : this(
id = mutation.id,
name = mutation.name,
avatar = mutation.avatar.let(::Avatar),
banner = mutation.bannerImage?.ifBlank { null },
displayAdultContent = mutation.options?.displayAdultContent ?: false
displayAdultContent = mutation.options?.displayAdultContent ?: false,
profileColor = mutation.options?.profileColor?.ifBlank { null }
)

@Serializable
Expand Down
92 changes: 84 additions & 8 deletions composeApp/src/commonMain/kotlin/dev/datlag/aniflow/App.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
package dev.datlag.aniflow

import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import com.materialkolor.*
import dev.chrisbanes.haze.HazeState
import dev.datlag.aniflow.common.toComposeColor
import dev.datlag.aniflow.settings.Settings
import dev.datlag.aniflow.settings.model.AppSettings
import dev.datlag.aniflow.ui.theme.Colors
import dev.datlag.aniflow.ui.theme.CommonSchemeTheme
import dev.datlag.tooling.compose.toTypography
import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle
import dev.icerock.moko.resources.compose.asFont
import org.kodein.di.DI
import org.kodein.di.instance

val LocalDarkMode = compositionLocalOf<Boolean> { error("No dark mode state provided") }
val LocalEdgeToEdge = staticCompositionLocalOf<Boolean> { false }
Expand All @@ -38,13 +49,73 @@ internal fun App(
colorScheme = if (systemDarkTheme) Colors.getDarkScheme() else Colors.getLightScheme(),
typography = ManropeFontFamily().toTypography()
) {
CommonSchemeTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground
) {
content()
val appSettings by di.instance<Settings.PlatformAppSettings>()
val savedColor by remember(appSettings) {
appSettings.color
}.collectAsStateWithLifecycle(null)
val seedColor = remember(savedColor) { savedColor?.toComposeColor() }
val dynamicColorScheme = if (seedColor != null) {
rememberDynamicColorScheme(
seedColor = seedColor,
isDark = LocalDarkMode.current,
style = PaletteStyle.TonalSpot,
contrastLevel = Contrast.Default.value,
isExtendedFidelity = false
)
} else {
MaterialTheme.colorScheme
}
val animationSpec: AnimationSpec<Color> = spring(stiffness = Spring.StiffnessLow)
val colorScheme = dynamicColorScheme.copy(
primary = dynamicColorScheme.primary.animate(animationSpec),
primaryContainer = dynamicColorScheme.primaryContainer.animate(animationSpec),
secondary = dynamicColorScheme.secondary.animate(animationSpec),
secondaryContainer = dynamicColorScheme.secondaryContainer.animate(animationSpec),
tertiary = dynamicColorScheme.tertiary.animate(animationSpec),
tertiaryContainer = dynamicColorScheme.tertiaryContainer.animate(animationSpec),
background = dynamicColorScheme.background.animate(animationSpec),
surface = dynamicColorScheme.surface.animate(animationSpec),
surfaceTint = dynamicColorScheme.surfaceTint.animate(animationSpec),
surfaceBright = dynamicColorScheme.surfaceBright.animate(animationSpec),
surfaceDim = dynamicColorScheme.surfaceDim.animate(animationSpec),
surfaceContainer = dynamicColorScheme.surfaceContainer.animate(animationSpec),
surfaceContainerHigh = dynamicColorScheme.surfaceContainerHigh.animate(animationSpec),
surfaceContainerHighest = dynamicColorScheme.surfaceContainerHighest.animate(animationSpec),
surfaceContainerLow = dynamicColorScheme.surfaceContainerLow.animate(animationSpec),
surfaceContainerLowest = dynamicColorScheme.surfaceContainerLowest.animate(animationSpec),
surfaceVariant = dynamicColorScheme.surfaceVariant.animate(animationSpec),
error = dynamicColorScheme.error.animate(animationSpec),
errorContainer = dynamicColorScheme.errorContainer.animate(animationSpec),
onPrimary = dynamicColorScheme.onPrimary.animate(animationSpec),
onPrimaryContainer = dynamicColorScheme.onPrimaryContainer.animate(animationSpec),
onSecondary = dynamicColorScheme.onSecondary.animate(animationSpec),
onSecondaryContainer = dynamicColorScheme.onSecondaryContainer.animate(animationSpec),
onTertiary = dynamicColorScheme.onTertiary.animate(animationSpec),
onTertiaryContainer = dynamicColorScheme.onTertiaryContainer.animate(animationSpec),
onBackground = dynamicColorScheme.onBackground.animate(animationSpec),
onSurface = dynamicColorScheme.onSurface.animate(animationSpec),
onSurfaceVariant = dynamicColorScheme.onSurfaceVariant.animate(animationSpec),
onError = dynamicColorScheme.onError.animate(animationSpec),
onErrorContainer = dynamicColorScheme.onErrorContainer.animate(animationSpec),
inversePrimary = dynamicColorScheme.inversePrimary.animate(animationSpec),
inverseSurface = dynamicColorScheme.inverseSurface.animate(animationSpec),
inverseOnSurface = dynamicColorScheme.inverseOnSurface.animate(animationSpec),
outline = dynamicColorScheme.outline.animate(animationSpec),
outlineVariant = dynamicColorScheme.outlineVariant.animate(animationSpec),
scrim = dynamicColorScheme.scrim.animate(animationSpec),
)

MaterialTheme(
colorScheme = colorScheme
) {
CommonSchemeTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground
) {
content()
}
}
}
}
Expand Down Expand Up @@ -80,4 +151,9 @@ fun ManropeFontFamily(): FontFamily {
}

@Composable
expect fun SystemAppearance(isDark: Boolean = LocalDarkMode.current)
expect fun SystemAppearance(isDark: Boolean = LocalDarkMode.current)

@Composable
private fun Color.animate(animationSpec: AnimationSpec<Color>): Color {
return animateColorAsState(this, animationSpec).value
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.datlag.aniflow.common

import androidx.compose.ui.graphics.Color
import dev.datlag.aniflow.settings.model.AppSettings

@OptIn(ExperimentalStdlibApi::class)
fun AppSettings.Color.toComposeColor() = Color(
this.hex.substringAfter('#').hexToLong() or 0x00000000FF000000
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import dev.datlag.aniflow.anilist.ViewerQuery
import dev.datlag.aniflow.anilist.model.User
import dev.datlag.aniflow.model.safeFirstOrNull
import dev.datlag.aniflow.settings.Settings
import dev.datlag.aniflow.settings.model.AppSettings
import dev.datlag.tooling.async.suspendCatching
import dev.datlag.tooling.compose.withMainContext
import kotlinx.coroutines.flow.*
Expand Down Expand Up @@ -49,7 +50,10 @@ class UserHelper(
val user = latestUser.transform { user ->
emit(
user?.also {
appSettings.setAdultContent(it.displayAdultContent)
appSettings.setData(
adultContent = it.displayAdultContent,
color = AppSettings.Color.fromString(it.profileColor)
)
}
)
}
Expand Down Expand Up @@ -85,6 +89,20 @@ class UserHelper(
)
}

suspend fun updateProfileColorSetting(value: AppSettings.Color?) {
appSettings.setColor(value)

if (value != null) {
changedUser.emit(
client.mutation(
ViewerMutation(
color = Optional.present(value.label)
)
).execute().data?.UpdateUser?.let(::User)
)
}
}

private suspend fun updateStoredToken(tokenResponse: AccessTokenResponse) {
userSettings.setAniListTokens(
access = tokenResponse.access_token,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.datlag.aniflow.ui.navigation.screen.initial.settings

import dev.datlag.aniflow.anilist.model.User
import dev.datlag.aniflow.settings.model.AppSettings
import dev.datlag.aniflow.ui.navigation.Component
import kotlinx.coroutines.flow.Flow

Expand All @@ -9,4 +10,5 @@ interface SettingsComponent : Component {
val adultContent: Flow<Boolean>

fun changeAdultContent(value: Boolean)
fun changeProfileColor(value: AppSettings.Color?)
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
package dev.datlag.aniflow.ui.navigation.screen.initial.settings

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.NoAdultContent
import androidx.compose.material3.*
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
Expand All @@ -25,10 +33,14 @@ import dev.datlag.aniflow.LocalHaze
import dev.datlag.aniflow.LocalPaddingValues
import dev.datlag.aniflow.SharedRes
import dev.datlag.aniflow.common.plus
import dev.datlag.aniflow.common.toComposeColor
import dev.datlag.aniflow.other.StateSaver
import dev.datlag.aniflow.settings.model.AppSettings
import dev.datlag.tooling.compose.onClick
import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle
import dev.icerock.moko.resources.compose.stringResource

@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3WindowSizeClassApi::class)
@Composable
fun SettingsScreen(component: SettingsComponent) {
val padding = PaddingValues(16.dp)
Expand Down Expand Up @@ -89,8 +101,7 @@ fun SettingsScreen(component: SettingsComponent) {
contentDescription = null,
)
Text(
text = stringResource(SharedRes.strings.adult_content_setting),
fontWeight = FontWeight.SemiBold
text = stringResource(SharedRes.strings.adult_content_setting)
)
Spacer(modifier = Modifier.weight(1F))
Switch(
Expand All @@ -108,6 +119,34 @@ fun SettingsScreen(component: SettingsComponent) {
)
}
}
item {
Text(
modifier = Modifier.padding(vertical = 16.dp),
text = "Profile Color",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.SemiBold
)
}
item {
FlowRow(
modifier = Modifier.fillParentMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
maxItemsInEachRow = when (calculateWindowSizeClass().widthSizeClass) {
WindowWidthSizeClass.Compact -> 4
else -> Int.MAX_VALUE
}
) {
AppSettings.Color.all.forEach {
ColorItem(
color = it,
onClick = { chosen ->
component.changeProfileColor(chosen)
}
)
}
}
}
}

DisposableEffect(listState) {
Expand All @@ -116,4 +155,19 @@ fun SettingsScreen(component: SettingsComponent) {
StateSaver.List.settingsOverviewOffset = listState.firstVisibleItemScrollOffset
}
}
}

@OptIn(ExperimentalStdlibApi::class)
@Composable
fun ColorItem(
color: AppSettings.Color,
onClick: (AppSettings.Color) -> Unit
) {
Card(
modifier = Modifier.size(48.dp),
onClick = { onClick(color) },
colors = CardDefaults.cardColors(containerColor = color.toComposeColor()),
shape = CircleShape,
content = { }
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dev.datlag.aniflow.anilist.model.User
import dev.datlag.aniflow.common.onRender
import dev.datlag.aniflow.other.UserHelper
import dev.datlag.aniflow.settings.Settings
import dev.datlag.aniflow.settings.model.AppSettings
import dev.datlag.tooling.compose.ioDispatcher
import dev.datlag.tooling.decompose.ioScope
import kotlinx.coroutines.flow.*
Expand Down Expand Up @@ -35,4 +36,10 @@ class SettingsScreenComponent(
userHelper.updateAdultSetting(value)
}
}

override fun changeProfileColor(value: AppSettings.Color?) {
launchIO {
userHelper.updateProfileColorSetting(value)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import okio.BufferedSink
import okio.BufferedSource

data object AppSettingsSerializer : OkioSerializer<AppSettings> {
override val defaultValue: AppSettings = AppSettings()
override val defaultValue: AppSettings = AppSettings(
color = null
)

@OptIn(ExperimentalSerializationApi::class)
private val protobuf = ProtoBuf {
Expand Down
Loading

0 comments on commit 765056a

Please sign in to comment.