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-systemuicontroller:$accompanist_version"
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version" implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
implementation "com.google.accompanist:accompanist-navigation-animation:$accompanist_version" implementation "com.google.accompanist:accompanist-navigation-animation:$accompanist_version"
implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version"
// KTOR - HTTP+WebSocket Library // KTOR - HTTP+WebSocket Library
implementation "io.ktor:ktor-client-core:$ktor_version" 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.Modifier
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import chat.revolt.api.settings.GlobalState
import chat.revolt.screens.SplashScreen import chat.revolt.screens.SplashScreen
import chat.revolt.screens.about.AboutScreen import chat.revolt.screens.about.AboutScreen
import chat.revolt.screens.about.AttributionScreen 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.GreeterScreen
import chat.revolt.screens.login.LoginScreen import chat.revolt.screens.login.LoginScreen
import chat.revolt.screens.login.MfaScreen import chat.revolt.screens.login.MfaScreen
import chat.revolt.screens.settings.AppearanceSettingsScreen
import chat.revolt.screens.settings.SettingsScreen import chat.revolt.screens.settings.SettingsScreen
import chat.revolt.ui.theme.RevoltTheme import chat.revolt.ui.theme.RevoltTheme
import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.AnimatedNavHost
@ -33,14 +35,7 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
RevoltTheme { AppEntrypoint()
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AppEntrypoint()
}
}
} }
} }
} }
@ -54,52 +49,62 @@ val RevoltTweenFloat: FiniteAnimationSpec<Float> = tween(400, easing = EaseInOut
fun AppEntrypoint() { fun AppEntrypoint() {
val navController = rememberAnimatedNavController() val navController = rememberAnimatedNavController()
AnimatedNavHost( RevoltTheme(
navController = navController, requestedTheme = GlobalState.theme,
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) } 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/greeting") { GreeterScreen(navController) }
composable("login/login") { LoginScreen(navController) } composable("login/login") { LoginScreen(navController) }
composable("login/mfa/{mfaTicket}/{allowedAuthTypes}") { backStackEntry -> composable("login/mfa/{mfaTicket}/{allowedAuthTypes}") { backStackEntry ->
val mfaTicket = backStackEntry.arguments?.getString("mfaTicket") ?: "" val mfaTicket = backStackEntry.arguments?.getString("mfaTicket") ?: ""
val allowedAuthTypes = val allowedAuthTypes =
backStackEntry.arguments?.getString("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.client.plugins.websocket.*
import io.ktor.websocket.* import io.ktor.websocket.*
import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.channels.consumeEach
import java.util.*
enum class DisconnectionState { enum class DisconnectionState {
Disconnected, Disconnected,
@ -64,7 +63,7 @@ object RealtimeSocket {
suspend fun sendPing() { suspend fun sendPing() {
if (disconnectionState != DisconnectionState.Connected) return 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)) socket?.send(RevoltJson.encodeToString(PingFrame.serializer(), pingPacket))
Log.d("RealtimeSocket", "Sent ping frame with ${pingPacket.data}") Log.d("RealtimeSocket", "Sent ping frame with ${pingPacket.data}")
} }
@ -189,8 +188,8 @@ object RealtimeSocket {
Log.d("RealtimeSocket", "Registered channel callback for $channelId.") Log.d("RealtimeSocket", "Registered channel callback for $channelId.")
} }
fun unregisterChannelCallback(channelId: String, callback: ChannelCallback) { fun unregisterChannelCallback(channelId: String) {
channelCallbacks.remove(channelId, callback) channelCallbacks.remove(channelId)
Log.d("RealtimeSocket", "Unregistered channel callback for $channelId") Log.d("RealtimeSocket", "Unregistered channel callback for $channelId")
} }

View File

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

View File

@ -11,7 +11,7 @@ data class AuthorizationFrame(
@Serializable @Serializable
data class PingFrame( data class PingFrame(
val type: String, val type: String,
val data: Int val data: Long
) )
@Serializable @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,33 +1,56 @@
package chat.revolt.components.generic package chat.revolt.components.generic
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.revolt.R
@Composable @Composable
fun PageHeader( fun PageHeader(
text: String, text: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showBackButton: Boolean = false,
onBackButtonClicked: () -> Unit = {},
) { ) {
Text( Row(
text = text, verticalAlignment = Alignment.CenterVertically
style = MaterialTheme.typography.displaySmall.copy( ) {
fontWeight = FontWeight.Bold, if (showBackButton) {
textAlign = TextAlign.Left, IconButton(onClick = onBackButtonClicked) {
fontSize = 24.sp Icon(
), modifier = modifier,
modifier = modifier imageVector = Icons.Default.ArrowBack,
.padding(horizontal = 15.dp, vertical = 15.dp) contentDescription = stringResource(id = R.string.back)
.fillMaxWidth(), )
) }
}
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 @Preview
@ -35,3 +58,9 @@ fun PageHeader(
fun PageHeaderPreview() { fun PageHeaderPreview() {
PageHeader(text = "Page Header") 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign 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) { fun AnyLink(text: String, action: () -> Unit, modifier: Modifier = Modifier) {
Text( Text(
text = text, text = text,
color = Color(0xaaffffff), color = MaterialTheme.colorScheme.onBackground.copy(
alpha = 0.5f
),
style = MaterialTheme.typography.titleMedium.copy( style = MaterialTheme.typography.titleMedium.copy(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontWeight = FontWeight.Normal, 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.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -21,8 +22,12 @@ import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController import androidx.navigation.NavController
import chat.revolt.R import chat.revolt.R
import chat.revolt.api.RevoltAPI 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.components.screens.splash.DisconnectedScreen
import chat.revolt.persistence.KVStorage 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.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -82,18 +87,29 @@ class SplashScreenViewModel @Inject constructor(
setNavigateTo("login") setNavigateTo("login")
} else { } else {
RevoltAPI.loginAs(token) RevoltAPI.loginAs(token)
loadSettings()
setNavigateTo("home") setNavigateTo("home")
} }
} }
} }
private fun loadSettings() {
viewModelScope.launch {
SyncedSettings.fetch()
SyncedSettings.android.theme?.let { GlobalState.setTheme(Theme.valueOf(it)) }
}
}
init { init {
checkLoggedInState() checkLoggedInState()
} }
} }
@Composable @Composable
fun SplashScreen(navController: NavController, viewModel: SplashScreenViewModel = hiltViewModel()) { fun SplashScreen(
navController: NavController,
viewModel: SplashScreenViewModel = hiltViewModel()
) {
if (!viewModel.isConnected) { if (!viewModel.isConnected) {
DisconnectedScreen( DisconnectedScreen(
onRetry = { onRetry = {
@ -105,6 +121,7 @@ fun SplashScreen(navController: NavController, viewModel: SplashScreenViewModel
Column( Column(
modifier = Modifier modifier = Modifier
.background(color = RevoltColorScheme.background)
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight(), .fillMaxHeight(),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
@ -169,7 +168,9 @@ fun MfaScreen(
Text( Text(
text = stringResource(R.string.mfa_interstitial_lead), text = stringResource(R.string.mfa_interstitial_lead),
color = Color(0xaaffffff), color = MaterialTheme.colorScheme.onBackground.copy(
alpha = 0.5f
),
style = MaterialTheme.typography.titleMedium.copy( style = MaterialTheme.typography.titleMedium.copy(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
@ -206,7 +207,9 @@ fun MfaScreen(
) { ) {
Text( Text(
text = stringResource(R.string.mfa_totp_lead), text = stringResource(R.string.mfa_totp_lead),
color = Color(0xaaffffff), color = MaterialTheme.colorScheme.onBackground.copy(
alpha = 0.5f
),
style = MaterialTheme.typography.titleMedium.copy( style = MaterialTheme.typography.titleMedium.copy(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
@ -244,7 +247,9 @@ fun MfaScreen(
) { ) {
Text( Text(
text = stringResource(R.string.mfa_recovery_lead), text = stringResource(R.string.mfa_recovery_lead),
color = Color(0xaaffffff), color = MaterialTheme.colorScheme.onBackground.copy(
alpha = 0.5f
),
style = MaterialTheme.typography.titleMedium.copy( style = MaterialTheme.typography.titleMedium.copy(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontWeight = FontWeight.Normal, 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.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button import androidx.compose.material.icons.Icons
import androidx.compose.material3.ElevatedButton import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import chat.revolt.R import chat.revolt.R
import chat.revolt.components.generic.PageHeader import chat.revolt.components.generic.PageHeader
import chat.revolt.components.screens.settings.SettingsCategory
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
@ -21,20 +25,51 @@ fun SettingsScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
) { ) {
PageHeader(stringResource(id = R.string.settings)) PageHeader(
ElevatedButton( text = stringResource(id = R.string.settings),
modifier = Modifier.fillMaxWidth(), showBackButton = true,
onClick = { onBackButtonClicked = {
navController.popBackStack() navController.popBackStack()
}) { })
Text(text = stringResource(id = R.string.back))
} Column(
Button( modifier = Modifier
modifier = Modifier.fillMaxWidth(), .fillMaxSize()
onClick = { .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") 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.app.Activity
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.*
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -15,43 +12,78 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
const val FORCE_ANDROID_DEFAULTS = false val RevoltColorScheme = darkColorScheme(
const val FOREGROUND = 0xffffffff
val DarkColorScheme = darkColorScheme(
primary = Color(0xfffe4654), primary = Color(0xfffe4654),
onPrimary = Color(FOREGROUND), onPrimary = Color(0xffffffff),
secondary = Color(0xfffd6671), secondary = Color(0xfffd6671),
onSecondary = Color(FOREGROUND), onSecondary = Color(0xffffffff),
tertiary = Color(0xffff6667), tertiary = Color(0xffff6667),
onTertiary = Color(FOREGROUND), onTertiary = Color(0xffffffff),
background = Color(0xff101823), background = Color(0xff101823),
onBackground = Color(FOREGROUND), onBackground = Color(0xffffffff),
surfaceVariant = Color(0xff172333), surfaceVariant = Color(0xff172333),
onSurfaceVariant = Color(FOREGROUND), onSurfaceVariant = Color(0xffffffff),
surface = Color(0xff111a26), 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 @Composable
fun RevoltTheme( fun RevoltTheme(
darkTheme: Boolean = isSystemInDarkTheme(), requestedTheme: Theme,
// Dynamic color is available on Android 12+
dynamicColor: Boolean = false,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val context = LocalContext.current
val systemInDarkTheme = isSystemInDarkTheme()
val m3Supported = systemSupportsDynamicColors()
val colorScheme = when { val colorScheme = when {
dynamicColor && darkTheme && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { m3Supported && requestedTheme == Theme.M3Dynamic && systemInDarkTheme -> dynamicDarkColorScheme(
val context = LocalContext.current context
dynamicDarkColorScheme(context) )
} m3Supported && requestedTheme == Theme.M3Dynamic && !systemInDarkTheme -> dynamicLightColorScheme(
dynamicColor && !darkTheme && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { context
val context = LocalContext.current )
dynamicLightColorScheme(context) requestedTheme == Theme.Revolt -> RevoltColorScheme
} requestedTheme == Theme.Light -> LightColorScheme
else -> DarkColorScheme requestedTheme == Theme.Amoled -> AmoledColorScheme
requestedTheme == Theme.None && systemInDarkTheme -> RevoltColorScheme
requestedTheme == Theme.None && !systemInDarkTheme -> LightColorScheme
else -> RevoltColorScheme
} }
val view = LocalView.current val view = LocalView.current
if (!view.isInEditMode) { if (!view.isInEditMode) {
SideEffect { SideEffect {
@ -62,15 +94,13 @@ fun RevoltTheme(
} }
} }
if (FORCE_ANDROID_DEFAULTS) { MaterialTheme(
MaterialTheme( colorScheme = colorScheme,
content = content typography = RevoltTypography,
) content = content
} else { )
MaterialTheme( }
colorScheme = colorScheme,
typography = RevoltTypography, fun systemSupportsDynamicColors(): Boolean {
content = content return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
)
}
} }

View File

@ -33,7 +33,7 @@ val RevoltTypography = Typography(
), ),
displaySmall = TextStyle( displaySmall = TextStyle(
fontFamily = Inter, fontFamily = Inter,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Bold,
fontSize = 36.sp fontSize = 36.sp
), ),
@ -49,39 +49,39 @@ val RevoltTypography = Typography(
), ),
headlineSmall = TextStyle( headlineSmall = TextStyle(
fontFamily = Inter, fontFamily = Inter,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Bold,
fontSize = 24.sp fontSize = 24.sp
), ),
titleLarge = TextStyle( titleLarge = TextStyle(
fontFamily = Inter, fontFamily = Inter,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.SemiBold,
fontSize = 22.sp fontSize = 22.sp
), ),
titleMedium = TextStyle( titleMedium = TextStyle(
fontFamily = Inter, fontFamily = Inter,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.SemiBold,
fontSize = 16.sp fontSize = 16.sp
), ),
titleSmall = TextStyle( titleSmall = TextStyle(
fontFamily = Inter, fontFamily = Inter,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Bold,
fontSize = 14.sp fontSize = 14.sp
), ),
labelLarge = TextStyle( labelLarge = TextStyle(
fontFamily = Inter, fontFamily = Inter,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.SemiBold,
fontSize = 14.sp fontSize = 14.sp
), ),
labelMedium = TextStyle( labelMedium = TextStyle(
fontFamily = Inter, fontFamily = Inter,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Bold,
fontSize = 12.sp fontSize = 12.sp
), ),
labelSmall = TextStyle( labelSmall = TextStyle(
fontFamily = Inter, fontFamily = Inter,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Bold,
fontSize = 11.sp 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 <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: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:strokeWidth="1"
android:fillColor="#fff" android:fillColor="#ffffff"
android:strokeColor="#fff"/> android:strokeColor="#ffffff"/>
</vector> </vector>

View File

@ -107,4 +107,14 @@
<string name="scroll_to_bottom">Scroll to bottom</string> <string name="scroll_to_bottom">Scroll to bottom</string>
<string name="settings">Settings</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> </resources>