feat: theme picker and settings sync 🔥🔥🔥🔥🔥

This commit is contained in:
Infi 2023-01-30 00:55:18 +01:00
parent 63227d8d0f
commit 023ac6e5cb
28 changed files with 710 additions and 174 deletions

View File

@ -75,6 +75,7 @@ dependencies {
implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version"
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
implementation "com.google.accompanist:accompanist-navigation-animation:$accompanist_version"
implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version"
// KTOR - HTTP+WebSocket Library
implementation "io.ktor:ktor-client-core:$ktor_version"

View File

@ -13,6 +13,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import chat.revolt.api.settings.GlobalState
import chat.revolt.screens.SplashScreen
import chat.revolt.screens.about.AboutScreen
import chat.revolt.screens.about.AttributionScreen
@ -21,6 +22,7 @@ import chat.revolt.screens.chat.ChatRouterScreen
import chat.revolt.screens.login.GreeterScreen
import chat.revolt.screens.login.LoginScreen
import chat.revolt.screens.login.MfaScreen
import chat.revolt.screens.settings.AppearanceSettingsScreen
import chat.revolt.screens.settings.SettingsScreen
import chat.revolt.ui.theme.RevoltTheme
import com.google.accompanist.navigation.animation.AnimatedNavHost
@ -33,14 +35,7 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RevoltTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AppEntrypoint()
}
}
AppEntrypoint()
}
}
}
@ -54,52 +49,62 @@ val RevoltTweenFloat: FiniteAnimationSpec<Float> = tween(400, easing = EaseInOut
fun AppEntrypoint() {
val navController = rememberAnimatedNavController()
AnimatedNavHost(
navController = navController,
startDestination = "splash",
enterTransition = {
slideIntoContainer(
AnimatedContentScope.SlideDirection.Left,
animationSpec = RevoltTweenInt
) + fadeIn(animationSpec = RevoltTweenFloat)
},
exitTransition = {
slideOutOfContainer(
AnimatedContentScope.SlideDirection.Left,
animationSpec = RevoltTweenInt
) + fadeOut(animationSpec = RevoltTweenFloat)
},
popEnterTransition = {
slideIntoContainer(
AnimatedContentScope.SlideDirection.Right,
animationSpec = RevoltTweenInt
) + fadeIn(animationSpec = RevoltTweenFloat)
},
popExitTransition = {
slideOutOfContainer(
AnimatedContentScope.SlideDirection.Right,
animationSpec = RevoltTweenInt
) + fadeOut(animationSpec = RevoltTweenFloat)
}
RevoltTheme(
requestedTheme = GlobalState.theme,
) {
composable("splash") { SplashScreen(navController) }
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AnimatedNavHost(
navController = navController,
startDestination = "splash",
enterTransition = {
slideIntoContainer(
AnimatedContentScope.SlideDirection.Left,
animationSpec = RevoltTweenInt
) + fadeIn(animationSpec = RevoltTweenFloat)
},
exitTransition = {
slideOutOfContainer(
AnimatedContentScope.SlideDirection.Left,
animationSpec = RevoltTweenInt
) + fadeOut(animationSpec = RevoltTweenFloat)
},
popEnterTransition = {
slideIntoContainer(
AnimatedContentScope.SlideDirection.Right,
animationSpec = RevoltTweenInt
) + fadeIn(animationSpec = RevoltTweenFloat)
},
popExitTransition = {
slideOutOfContainer(
AnimatedContentScope.SlideDirection.Right,
animationSpec = RevoltTweenInt
) + fadeOut(animationSpec = RevoltTweenFloat)
}
) {
composable("splash") { SplashScreen(navController) }
composable("login/greeting") { GreeterScreen(navController) }
composable("login/login") { LoginScreen(navController) }
composable("login/mfa/{mfaTicket}/{allowedAuthTypes}") { backStackEntry ->
val mfaTicket = backStackEntry.arguments?.getString("mfaTicket") ?: ""
val allowedAuthTypes =
backStackEntry.arguments?.getString("allowedAuthTypes") ?: ""
composable("login/greeting") { GreeterScreen(navController) }
composable("login/login") { LoginScreen(navController) }
composable("login/mfa/{mfaTicket}/{allowedAuthTypes}") { backStackEntry ->
val mfaTicket = backStackEntry.arguments?.getString("mfaTicket") ?: ""
val allowedAuthTypes =
backStackEntry.arguments?.getString("allowedAuthTypes") ?: ""
MfaScreen(navController, allowedAuthTypes, mfaTicket)
MfaScreen(navController, allowedAuthTypes, mfaTicket)
}
composable("chat") { ChatRouterScreen(navController) }
composable("settings") { SettingsScreen(navController) }
composable("settings/appearance") { AppearanceSettingsScreen(navController) }
composable("about") { AboutScreen(navController) }
composable("about/oss") { AttributionScreen(navController) }
composable("about/placeholder") { PlaceholderScreen(navController) }
}
}
composable("chat") { ChatRouterScreen(navController) }
composable("settings") { SettingsScreen(navController) }
composable("about") { AboutScreen(navController) }
composable("about/oss") { AttributionScreen(navController) }
composable("about/placeholder") { PlaceholderScreen(navController) }
}
}

View File

@ -14,7 +14,6 @@ import chat.revolt.api.realtime.frames.sendable.PingFrame
import io.ktor.client.plugins.websocket.*
import io.ktor.websocket.*
import kotlinx.coroutines.channels.consumeEach
import java.util.*
enum class DisconnectionState {
Disconnected,
@ -64,7 +63,7 @@ object RealtimeSocket {
suspend fun sendPing() {
if (disconnectionState != DisconnectionState.Connected) return
val pingPacket = PingFrame("Ping", Calendar.getInstance().timeInMillis.toInt())
val pingPacket = PingFrame("Ping", System.currentTimeMillis())
socket?.send(RevoltJson.encodeToString(PingFrame.serializer(), pingPacket))
Log.d("RealtimeSocket", "Sent ping frame with ${pingPacket.data}")
}
@ -167,7 +166,7 @@ object RealtimeSocket {
}
}
}
private fun invalidateAllChannelStates() {
channelCallbacks.forEach { (_, callback) ->
callback.onStateInvalidate()
@ -189,8 +188,8 @@ object RealtimeSocket {
Log.d("RealtimeSocket", "Registered channel callback for $channelId.")
}
fun unregisterChannelCallback(channelId: String, callback: ChannelCallback) {
channelCallbacks.remove(channelId, callback)
fun unregisterChannelCallback(channelId: String) {
channelCallbacks.remove(channelId)
Log.d("RealtimeSocket", "Unregistered channel callback for $channelId")
}

View File

@ -25,7 +25,7 @@ data class BulkFrame(
@Serializable
data class PongFrame(
val type: String = "Pong",
val data: Int
val data: Long
)
@Serializable

View File

@ -11,7 +11,7 @@ data class AuthorizationFrame(
@Serializable
data class PingFrame(
val type: String,
val data: Int
val data: Long
)
@Serializable

View File

@ -0,0 +1,68 @@
package chat.revolt.api.routes.sync
import chat.revolt.api.RevoltAPI
import chat.revolt.api.RevoltHttp
import chat.revolt.api.RevoltJson
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.JsonArray
@Serializable
data class SyncedSetting(val timestamp: Long, val value: String)
suspend fun getKeys(vararg keys: String): Map<String, SyncedSetting> {
val response = RevoltHttp.post("/sync/settings/fetch") {
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
// format: {"keys": ["key1", "key2"]}
setBody(
RevoltJson.encodeToString(
MapSerializer(
String.serializer(),
ListSerializer(String.serializer())
),
mapOf("keys" to keys.toList())
)
)
}.bodyAsText()
return RevoltJson.decodeFromString(
MapSerializer(
String.serializer(),
JsonArray.serializer()
),
response
).mapValues { (_, value) ->
SyncedSetting(
timestamp = value[0].toString().toLong(),
value = value[1]
.toString()
.removeSurrounding("\"")
.replace("\\\"", "\"")
.replace("\\\\", "\\") // the revolt API is so scuffed i can't even make this up
)
}
}
suspend fun setKey(key: String, value: String) {
RevoltHttp.post("/sync/settings/set") {
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
parameter("timestamp", System.currentTimeMillis())
// format: {"key": "value"}
setBody(
RevoltJson.encodeToString(
MapSerializer(
String.serializer(),
String.serializer()
),
mapOf(key to value)
)
)
}
}

View File

@ -0,0 +1,17 @@
package chat.revolt.api.schemas
import kotlinx.serialization.Serializable
@Serializable
data class OrderingSettings(
val servers: List<String> = emptyList(),
)
@Serializable
data class AndroidSpecificSettings(
/**
* The theme to use for the app.
* Can be one of { None, Revolt, Light, M3Dynamic, Amoled }
*/
var theme: String? = null,
)

View File

@ -0,0 +1,14 @@
package chat.revolt.api.settings
import androidx.compose.runtime.mutableStateOf
import chat.revolt.ui.theme.Theme
object GlobalState {
private var _theme = mutableStateOf(Theme.Revolt)
val theme
get() = _theme.value
fun setTheme(theme: Theme) {
_theme.value = theme
}
}

View File

@ -0,0 +1,46 @@
package chat.revolt.api.settings
import androidx.compose.runtime.mutableStateOf
import chat.revolt.api.RevoltJson
import chat.revolt.api.routes.sync.getKeys
import chat.revolt.api.routes.sync.setKey
import chat.revolt.api.schemas.AndroidSpecificSettings
import chat.revolt.api.schemas.OrderingSettings
object SyncedSettings {
private val _ordering = mutableStateOf(OrderingSettings())
private val _android = mutableStateOf(AndroidSpecificSettings("None"))
val ordering: OrderingSettings
get() = _ordering.value
val android: AndroidSpecificSettings
get() = _android.value
suspend fun fetch() {
val settings = getKeys("ordering", "android")
settings["ordering"]?.let {
_ordering.value = RevoltJson.decodeFromString(
OrderingSettings.serializer(),
it.value
)
}
settings["android"]?.let {
_android.value = RevoltJson.decodeFromString(
AndroidSpecificSettings.serializer(),
it.value
)
}
}
suspend fun updateOrdering(value: OrderingSettings) {
_ordering.value = value
setKey("ordering", RevoltJson.encodeToString(OrderingSettings.serializer(), value))
}
suspend fun updateAndroid(value: AndroidSpecificSettings) {
_android.value = value
setKey("android", RevoltJson.encodeToString(AndroidSpecificSettings.serializer(), value))
}
}

View File

@ -1,37 +1,66 @@
package chat.revolt.components.generic
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.revolt.R
@Composable
fun PageHeader(
text: String,
modifier: Modifier = Modifier,
showBackButton: Boolean = false,
onBackButtonClicked: () -> Unit = {},
) {
Text(
text = text,
style = MaterialTheme.typography.displaySmall.copy(
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Left,
fontSize = 24.sp
),
modifier = modifier
.padding(horizontal = 15.dp, vertical = 15.dp)
.fillMaxWidth(),
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
if (showBackButton) {
IconButton(onClick = onBackButtonClicked) {
Icon(
modifier = modifier,
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(id = R.string.back)
)
}
}
Text(
text = text,
style = MaterialTheme.typography.displaySmall.copy(
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Left,
fontSize = 24.sp
),
modifier = modifier
.padding(horizontal = 15.dp, vertical = 15.dp)
.fillMaxWidth(),
)
}
}
@Preview
@Composable
fun PageHeaderPreview() {
PageHeader(text = "Page Header")
}
@Preview
@Composable
fun PageHeaderPreviewWithBackButton() {
PageHeader(text = "Page Header", showBackButton = true)
}

View File

@ -8,7 +8,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@ -32,7 +31,9 @@ fun Weblink(text: String, url: String, modifier: Modifier = Modifier) {
fun AnyLink(text: String, action: () -> Unit, modifier: Modifier = Modifier) {
Text(
text = text,
color = Color(0xaaffffff),
color = MaterialTheme.colorScheme.onBackground.copy(
alpha = 0.5f
),
style = MaterialTheme.typography.titleMedium.copy(
textAlign = TextAlign.Center,
fontWeight = FontWeight.Normal,

View File

@ -0,0 +1,67 @@
package chat.revolt.components.screens.settings
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun SettingsCategory(
icon: @Composable (Modifier) -> Unit,
label: @Composable (TextStyle) -> Unit,
onClick: () -> Unit,
) {
Box(modifier = Modifier.padding(bottom = 8.dp)) {
Row(
modifier = Modifier
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.surface)
.clickable(onClick = onClick)
.padding(all = 4.dp)
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
icon(Modifier.padding(end = 16.dp))
label(
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold,
)
)
}
}
}
@Preview
@Composable
fun SettingsCategoryPreview() {
SettingsCategory(
icon = { modifier ->
Icon(
modifier = modifier,
imageVector = Icons.Default.Person,
contentDescription = "Account"
)
},
label = { textStyle ->
Text(text = "Account", style = textStyle)
},
onClick = {}
)
}

View File

@ -0,0 +1,49 @@
package chat.revolt.components.screens.settings.appearance
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun ThemeChip(
modifier: Modifier = Modifier,
color: Color,
text: String,
selected: Boolean = false,
onClick: () -> Unit
) {
Column(
Modifier
.clip(MaterialTheme.shapes.medium)
.clickable(onClick = onClick)
.then(modifier)
.then(
if (selected)
Modifier
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f))
else Modifier
)
.padding(4.dp)
.padding(8.dp)
) {
Box(
modifier = Modifier
.clip(MaterialTheme.shapes.medium)
.background(color)
.height(60.dp)
.fillMaxWidth(1f)
)
Text(
text = text,
modifier = Modifier.padding(top = 8.dp),
style = MaterialTheme.typography.labelLarge
)
}
}

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -21,8 +22,12 @@ import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.api.RevoltAPI
import chat.revolt.api.settings.GlobalState
import chat.revolt.api.settings.SyncedSettings
import chat.revolt.components.screens.splash.DisconnectedScreen
import chat.revolt.persistence.KVStorage
import chat.revolt.ui.theme.RevoltColorScheme
import chat.revolt.ui.theme.Theme
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.launch
@ -82,18 +87,29 @@ class SplashScreenViewModel @Inject constructor(
setNavigateTo("login")
} else {
RevoltAPI.loginAs(token)
loadSettings()
setNavigateTo("home")
}
}
}
private fun loadSettings() {
viewModelScope.launch {
SyncedSettings.fetch()
SyncedSettings.android.theme?.let { GlobalState.setTheme(Theme.valueOf(it)) }
}
}
init {
checkLoggedInState()
}
}
@Composable
fun SplashScreen(navController: NavController, viewModel: SplashScreenViewModel = hiltViewModel()) {
fun SplashScreen(
navController: NavController,
viewModel: SplashScreenViewModel = hiltViewModel()
) {
if (!viewModel.isConnected) {
DisconnectedScreen(
onRetry = {
@ -105,6 +121,7 @@ fun SplashScreen(navController: NavController, viewModel: SplashScreenViewModel
Column(
modifier = Modifier
.background(color = RevoltColorScheme.background)
.fillMaxWidth()
.fillMaxHeight(),
verticalArrangement = Arrangement.Center,

View File

@ -11,7 +11,6 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@ -50,7 +49,9 @@ fun VersionItem(
Row(modifier) {
Text(
text = key,
color = Color(0xccffffff),
color = MaterialTheme.colorScheme.onBackground.copy(
alpha = 0.5f
),
style = MaterialTheme.typography.titleMedium.copy(
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold
@ -60,7 +61,9 @@ fun VersionItem(
)
Text(
text = value,
color = Color(0xccffffff),
color = MaterialTheme.colorScheme.onBackground.copy(
alpha = 0.5f
),
style = MaterialTheme.typography.titleMedium.copy(
textAlign = TextAlign.Center,
fontWeight = FontWeight.Normal
@ -117,7 +120,9 @@ fun AboutScreen(
if (viewModel.root.value == null) {
Text(
text = stringResource(R.string.loading),
color = Color(0xaaffffff),
color = MaterialTheme.colorScheme.onBackground.copy(
alpha = 0.5f
),
style = MaterialTheme.typography.titleMedium.copy(
textAlign = TextAlign.Center,
fontWeight = FontWeight.Normal

View File

@ -16,7 +16,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.ui.theme.DarkColorScheme
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
@ -39,10 +38,10 @@ fun AttributionScreen(navController: NavController) {
.fillMaxSize()
.weight(1f),
colors = LibraryDefaults.libraryColors(
backgroundColor = DarkColorScheme.background,
contentColor = DarkColorScheme.onBackground,
badgeBackgroundColor = DarkColorScheme.primary,
badgeContentColor = DarkColorScheme.onPrimary
backgroundColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground,
badgeBackgroundColor = MaterialTheme.colorScheme.primary,
badgeContentColor = MaterialTheme.colorScheme.onPrimary
)
)
Button(

View File

@ -7,7 +7,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@ -49,10 +48,8 @@ fun PlaceholderScreen(
Text(
text = stringResource(R.string.comingsoon_body),
color = Color(0xaaffffff),
style = MaterialTheme.typography.titleMedium.copy(
textAlign = TextAlign.Center,
fontWeight = FontWeight.Normal,
color = MaterialTheme.colorScheme.onBackground.copy(
alpha = 0.5f
),
modifier = Modifier
.padding(horizontal = 20.dp, vertical = 10.dp)

View File

@ -64,9 +64,9 @@ class ChannelScreenViewModel : ViewModel() {
val channel: Channel?
get() = _channel
private var _callbacks = mutableStateOf<RealtimeSocket.ChannelCallback?>(null)
val callbacks: RealtimeSocket.ChannelCallback?
get() = _callbacks.value
private var _callback = mutableStateOf<RealtimeSocket.ChannelCallback?>(null)
val callback: RealtimeSocket.ChannelCallback?
get() = _callback.value
private var _renderableMessages = mutableStateListOf<MessageSchema>()
val renderableMessages: List<MessageSchema>
@ -164,8 +164,8 @@ class ChannelScreenViewModel : ViewModel() {
}
private fun registerCallback() {
_callbacks.value = ChannelScreenCallback()
RealtimeSocket.registerChannelCallback(channel!!.id!!, callbacks!!)
_callback.value = ChannelScreenCallback()
RealtimeSocket.registerChannelCallback(channel!!.id!!, callback!!)
}
fun fetchMessages() {
@ -399,9 +399,7 @@ fun ChannelScreen(
DisposableEffect(channelId) {
onDispose {
viewModel.callbacks?.let {
RealtimeSocket.unregisterChannelCallback(channelId, it)
}
RealtimeSocket.unregisterChannelCallback(channelId)
}
}

View File

@ -12,7 +12,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
@ -84,7 +83,9 @@ fun GreeterScreen(navController: NavController) {
Text(
text = stringResource(R.string.login_onboarding_body),
color = Color(0xaaffffff),
color = MaterialTheme.colorScheme.onBackground.copy(
alpha = 0.5f
),
style = MaterialTheme.typography.titleMedium.copy(
textAlign = TextAlign.Center,
fontWeight = FontWeight.Normal,

View File

@ -14,7 +14,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
@ -169,7 +168,9 @@ fun MfaScreen(
Text(
text = stringResource(R.string.mfa_interstitial_lead),
color = Color(0xaaffffff),
color = MaterialTheme.colorScheme.onBackground.copy(
alpha = 0.5f
),
style = MaterialTheme.typography.titleMedium.copy(
textAlign = TextAlign.Center,
fontWeight = FontWeight.Normal,
@ -206,7 +207,9 @@ fun MfaScreen(
) {
Text(
text = stringResource(R.string.mfa_totp_lead),
color = Color(0xaaffffff),
color = MaterialTheme.colorScheme.onBackground.copy(
alpha = 0.5f
),
style = MaterialTheme.typography.titleMedium.copy(
textAlign = TextAlign.Center,
fontWeight = FontWeight.Normal,
@ -244,7 +247,9 @@ fun MfaScreen(
) {
Text(
text = stringResource(R.string.mfa_recovery_lead),
color = Color(0xaaffffff),
color = MaterialTheme.colorScheme.onBackground.copy(
alpha = 0.5f
),
style = MaterialTheme.typography.titleMedium.copy(
textAlign = TextAlign.Center,
fontWeight = FontWeight.Normal,

View File

@ -0,0 +1,145 @@
package chat.revolt.screens.settings
import android.widget.Toast
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.api.settings.GlobalState
import chat.revolt.api.settings.SyncedSettings
import chat.revolt.components.generic.PageHeader
import chat.revolt.components.screens.settings.appearance.ThemeChip
import chat.revolt.ui.theme.Theme
import chat.revolt.ui.theme.systemSupportsDynamicColors
import com.google.accompanist.flowlayout.FlowRow
import kotlinx.coroutines.launch
class AppearanceSettingsScreenViewModel : ViewModel() {
fun saveNewTheme(theme: Theme) {
viewModelScope.launch {
val android = SyncedSettings.android
android.theme = theme.toString()
SyncedSettings.updateAndroid(android)
}
}
}
@Composable
fun AppearanceSettingsScreen(
navController: NavController,
viewModel: AppearanceSettingsScreenViewModel = viewModel()
) {
val context = LocalContext.current
fun setNewTheme(theme: Theme) {
GlobalState.setTheme(theme)
viewModel.saveNewTheme(theme)
}
Column(
modifier = Modifier
.fillMaxSize()
) {
PageHeader(
text = stringResource(id = R.string.settings_appearance),
showBackButton = true,
onBackButtonClicked = {
navController.popBackStack()
})
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(20.dp)
) {
Text(
text = stringResource(id = R.string.settings_appearance_theme),
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(bottom = 10.dp)
)
FlowRow(
mainAxisSpacing = 10.dp,
crossAxisSpacing = 10.dp,
) {
ThemeChip(
color = Color(0xff172333),
text = stringResource(id = R.string.settings_appearance_theme_revolt),
selected = GlobalState.theme == Theme.Revolt,
modifier = Modifier.weight(1f),
) {
setNewTheme(Theme.Revolt)
}
ThemeChip(
color = Color(0xfff7f7f7),
text = stringResource(id = R.string.settings_appearance_theme_light),
selected = GlobalState.theme == Theme.Light,
modifier = Modifier.weight(1f),
) {
setNewTheme(Theme.Light)
}
ThemeChip(
color = Color(0xff000000),
text = stringResource(id = R.string.settings_appearance_theme_amoled),
selected = GlobalState.theme == Theme.Amoled,
modifier = Modifier.weight(1f),
) {
setNewTheme(Theme.Amoled)
}
ThemeChip(
color = if (isSystemInDarkTheme()) Color(0xff172333) else Color(0xfff7f7f7),
text = stringResource(id = R.string.settings_appearance_theme_none),
selected = GlobalState.theme == Theme.None,
modifier = Modifier.weight(1f),
) {
setNewTheme(Theme.None)
}
if (systemSupportsDynamicColors()) {
ThemeChip(
color = dynamicDarkColorScheme(LocalContext.current).primary,
text = stringResource(id = R.string.settings_appearance_theme_m3dynamic),
selected = GlobalState.theme == Theme.M3Dynamic,
modifier = Modifier.weight(1f),
) {
setNewTheme(Theme.M3Dynamic)
}
} else {
ThemeChip(
color = Color(0xffa0a0a0),
text = stringResource(id = R.string.settings_appearance_theme_m3dynamic_unsupported),
selected = false,
modifier = Modifier.weight(1f),
) {
Toast.makeText(
context,
context.getString(R.string.settings_appearance_theme_m3dynamic_unsupported_toast),
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
}

View File

@ -2,16 +2,20 @@ package chat.revolt.screens.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.components.generic.PageHeader
import chat.revolt.components.screens.settings.SettingsCategory
@Composable
fun SettingsScreen(
@ -21,20 +25,51 @@ fun SettingsScreen(
modifier = Modifier
.fillMaxSize()
) {
PageHeader(stringResource(id = R.string.settings))
ElevatedButton(
modifier = Modifier.fillMaxWidth(),
onClick = {
PageHeader(
text = stringResource(id = R.string.settings),
showBackButton = true,
onBackButtonClicked = {
navController.popBackStack()
}) {
Text(text = stringResource(id = R.string.back))
}
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
})
Column(
modifier = Modifier
.fillMaxSize()
.padding(10.dp)
) {
SettingsCategory(
icon = { modifier ->
Icon(
painter = painterResource(id = R.drawable.ic_palette_24dp),
contentDescription =
stringResource(id = R.string.settings_appearance),
modifier = modifier
)
},
label = { textStyle ->
Text(
text = stringResource(id = R.string.settings_appearance),
style = textStyle
)
})
{
navController.navigate("settings/appearance")
}
SettingsCategory(
icon = { modifier ->
Icon(
imageVector = Icons.Default.Info,
contentDescription = stringResource(id = R.string.about),
modifier = modifier
)
},
label = { textStyle ->
Text(text = stringResource(id = R.string.about), style = textStyle)
})
{
navController.navigate("about")
}) {
Text(text = stringResource(id = R.string.about))
}
}
}
}

View File

@ -1,11 +0,0 @@
package chat.revolt.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@ -3,10 +3,7 @@ package chat.revolt.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
@ -15,43 +12,78 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat
const val FORCE_ANDROID_DEFAULTS = false
const val FOREGROUND = 0xffffffff
val DarkColorScheme = darkColorScheme(
val RevoltColorScheme = darkColorScheme(
primary = Color(0xfffe4654),
onPrimary = Color(FOREGROUND),
onPrimary = Color(0xffffffff),
secondary = Color(0xfffd6671),
onSecondary = Color(FOREGROUND),
onSecondary = Color(0xffffffff),
tertiary = Color(0xffff6667),
onTertiary = Color(FOREGROUND),
onTertiary = Color(0xffffffff),
background = Color(0xff101823),
onBackground = Color(FOREGROUND),
onBackground = Color(0xffffffff),
surfaceVariant = Color(0xff172333),
onSurfaceVariant = Color(FOREGROUND),
onSurfaceVariant = Color(0xffffffff),
surface = Color(0xff111a26),
onSurface = Color(FOREGROUND),
onSurface = Color(0xffffffff),
)
val AmoledColorScheme = RevoltColorScheme.copy(
background = Color(0xff000000),
onBackground = Color(0xffffffff),
surfaceVariant = Color(0xff131313),
onSurfaceVariant = Color(0xffffffff),
surface = Color(0xff212121),
onSurface = Color(0xffffffff),
)
val LightColorScheme = lightColorScheme(
primary = Color(0xfffe4654),
onPrimary = Color(0xffffffff),
secondary = Color(0xfffd6671),
onSecondary = Color(0xffffffff),
tertiary = Color(0xffff6667),
onTertiary = Color(0xffffffff),
background = Color(0xffffffff),
onBackground = Color(0xff000000),
surfaceVariant = Color(0xffe6e6e6),
onSurfaceVariant = Color(0xff000000),
surface = Color(0xffdddddd),
onSurface = Color(0xff000000),
)
enum class Theme {
None,
Revolt,
Light,
M3Dynamic,
Amoled,
}
@Composable
fun RevoltTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = false,
requestedTheme: Theme,
content: @Composable () -> Unit
) {
val context = LocalContext.current
val systemInDarkTheme = isSystemInDarkTheme()
val m3Supported = systemSupportsDynamicColors()
val colorScheme = when {
dynamicColor && darkTheme && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
dynamicDarkColorScheme(context)
}
dynamicColor && !darkTheme && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
dynamicLightColorScheme(context)
}
else -> DarkColorScheme
m3Supported && requestedTheme == Theme.M3Dynamic && systemInDarkTheme -> dynamicDarkColorScheme(
context
)
m3Supported && requestedTheme == Theme.M3Dynamic && !systemInDarkTheme -> dynamicLightColorScheme(
context
)
requestedTheme == Theme.Revolt -> RevoltColorScheme
requestedTheme == Theme.Light -> LightColorScheme
requestedTheme == Theme.Amoled -> AmoledColorScheme
requestedTheme == Theme.None && systemInDarkTheme -> RevoltColorScheme
requestedTheme == Theme.None && !systemInDarkTheme -> LightColorScheme
else -> RevoltColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
@ -62,15 +94,13 @@ fun RevoltTheme(
}
}
if (FORCE_ANDROID_DEFAULTS) {
MaterialTheme(
content = content
)
} else {
MaterialTheme(
colorScheme = colorScheme,
typography = RevoltTypography,
content = content
)
}
MaterialTheme(
colorScheme = colorScheme,
typography = RevoltTypography,
content = content
)
}
fun systemSupportsDynamicColors(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
}

View File

@ -33,7 +33,7 @@ val RevoltTypography = Typography(
),
displaySmall = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontWeight = FontWeight.Bold,
fontSize = 36.sp
),
@ -49,39 +49,39 @@ val RevoltTypography = Typography(
),
headlineSmall = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Medium,
fontWeight = FontWeight.Bold,
fontSize = 24.sp
),
titleLarge = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontWeight = FontWeight.SemiBold,
fontSize = 22.sp
),
titleMedium = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Medium,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp
),
titleSmall = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Medium,
fontWeight = FontWeight.Bold,
fontSize = 14.sp
),
labelLarge = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Medium,
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp
),
labelMedium = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Medium,
fontWeight = FontWeight.Bold,
fontSize = 12.sp
),
labelSmall = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Medium,
fontWeight = FontWeight.Bold,
fontSize = 11.sp
),

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M17.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,9A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 17.5,12M14.5,8A1.5,1.5 0 0,1 13,6.5A1.5,1.5 0 0,1 14.5,5A1.5,1.5 0 0,1 16,6.5A1.5,1.5 0 0,1 14.5,8M9.5,8A1.5,1.5 0 0,1 8,6.5A1.5,1.5 0 0,1 9.5,5A1.5,1.5 0 0,1 11,6.5A1.5,1.5 0 0,1 9.5,8M6.5,12A1.5,1.5 0 0,1 5,10.5A1.5,1.5 0 0,1 6.5,9A1.5,1.5 0 0,1 8,10.5A1.5,1.5 0 0,1 6.5,12M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A1.5,1.5 0 0,0 13.5,19.5C13.5,19.11 13.35,18.76 13.11,18.5C12.88,18.23 12.73,17.88 12.73,17.5A1.5,1.5 0 0,1 14.23,16H16A5,5 0 0,0 21,11C21,6.58 16.97,3 12,3Z"
android:fillColor="#ffffff" />
</vector>

View File

@ -6,6 +6,6 @@
<path
android:pathData="M24.98,12.56c0,2.79 -1.52,4.46 -4.76,4.46L14.84,17.02L14.84,8.2L20.22,8.2C23.46,8.2 24.98,9.93 24.98,12.56ZM0.98,1.01 L6.18,8.22L6.18,36.58h8.67L14.84,23.15h2.08l7.4,13.43h9.78l-8.21,-14.09A10.35,10.35 0,0 0,33.8 12.21c0,-6.18 -4.36,-11.2 -13.07,-11.2ZM61.01,1.01L39.22,1.01L39.22,36.58L61.01,36.58L61.01,29.64L47.89,29.64v-7.8L59.49,21.84L59.49,15.15L47.89,15.15L47.89,8.21L61.01,8.21ZM82,27.87 L73.18,1.01L63.95,1.01L76.57,36.58L87.42,36.58L100.03,1.01L90.86,1.01ZM138.65,18.69c0,-10.69 -8.06,-18.19 -18.19,-18.19 -10.09,0 -18.3,7.5 -18.3,18.19a17.9,17.9 0,0 0,18.3 18.24A17.82,17.82 0,0 0,138.65 18.69ZM111.03,18.69c0,-6.34 3.65,-10.34 9.43,-10.34 5.68,0 9.38,4 9.38,10.34 0,6.23 -3.7,10.34 -9.38,10.34C114.68,29.03 111.03,24.93 111.03,18.69ZM143.47,1.01L143.47,36.58L163.49,36.58v-6.95L152.13,29.63L152.13,1.01ZM165.71,8.21h9.43L175.14,36.58h8.67L183.81,8.2h9.43v-7.2L165.71,1.01Z"
android:strokeWidth="1"
android:fillColor="#fff"
android:strokeColor="#fff"/>
android:fillColor="#ffffff"
android:strokeColor="#ffffff"/>
</vector>

View File

@ -107,4 +107,14 @@
<string name="scroll_to_bottom">Scroll to bottom</string>
<string name="settings">Settings</string>
<string name="settings_appearance">Appearance</string>
<string name="settings_appearance_theme">Theme</string>
<string name="settings_appearance_theme_none">System</string>
<string name="settings_appearance_theme_revolt">Revolt</string>
<string name="settings_appearance_theme_light">Light</string>
<string name="settings_appearance_theme_amoled">Pure Black</string>
<string name="settings_appearance_theme_m3dynamic">Material You</string>
<string name="settings_appearance_theme_m3dynamic_unsupported">Material You (unsupported)</string>
<string name="settings_appearance_theme_m3dynamic_unsupported_toast">Material You is not supported on this device.</string>
</resources>