This commit is contained in:
2026-03-02 18:49:17 +02:00
parent 2433322556
commit a59f2d61dc
76 changed files with 10212 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
package com.sffteam.voidclient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import kotlin.collections.plus
data class Session(
val client: String = "",
val location: String = "",
val current: Boolean = false,
val time: Long = 0L,
val info: String = ""
)
data class Settings(
val safeMode : Boolean = false,
val searchByPhone : String = "",
val incomingCall : String = "",
val chatsInvite : String = "",
val contentLevelAccess : Boolean = false,
val hidden : Boolean = false
)
object AccountManager {
private val _sessionsList = MutableStateFlow<List<Session>>(emptyList())
var sessionsList = _sessionsList.asStateFlow()
private val _settings = MutableStateFlow(Settings())
var settings = _settings.asStateFlow()
var logined: Boolean = false
var accountID: Long = 0L
var token: String = ""
var phone: String = ""
fun processSettings(settings : JsonObject) {
try {
val safeMode = settings["SAFE_MODE"]?.jsonPrimitive?.boolean
val searchByPhone = settings["SEARCH_BY_PHONE"]?.jsonPrimitive?.content
val incomingCall = settings["INCOMING_CALL"]?.jsonPrimitive?.content
val chatsInvite = settings["CHATS_INVITE"]?.jsonPrimitive?.content
val contentLevelAccess = settings["CONTENT_LEVEL_ACCESS"]?.jsonPrimitive?.boolean
val hidden = settings["HIDDEN"]?.jsonPrimitive?.boolean
val newSettings = Settings(safeMode!!, searchByPhone!!, incomingCall!!, chatsInvite!!, contentLevelAccess!!, hidden!!)
_settings.update {
newSettings
}
} catch (e : Exception) {
println(e)
}
println("New settings! ${_settings.value}")
}
fun processSession(sessions: JsonArray) {
for (i in sessions) {
try {
var client: String = ""
var location: String = ""
var current: Boolean = false
var time: Long = 0L
var info: String = ""
try {
client = i.jsonObject["client"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
println("0msg")
}
try {
location =
i.jsonObject["location"]!!.jsonPrimitive.content
} catch (e: Exception) {
println("1msg")
println(e)
}
try {
if (i.jsonObject.containsKey("current")) {
current = i.jsonObject["current"]!!.jsonPrimitive.boolean
}
} catch (e: Exception) {
println("5msg")
println(e)
}
try {
time =
i.jsonObject["time"]!!.jsonPrimitive.long
} catch (e: Exception) {
println("1msg")
println(e)
}
try {
info =
i.jsonObject["info"]!!.jsonPrimitive.content
} catch (e: Exception) {
println("1msg")
println(e)
}
val currentList = listOf(
Session(client, location, current, time, info)
)
_sessionsList.update {
it.toList() + currentList
}
} catch (e: Exception) {
println(e)
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,385 @@
package com.sffteam.voidclient
import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
import androidx.compose.material.icons.filled.PhotoCamera
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import coil3.compose.AsyncImage
import com.sffteam.voidclient.ui.theme.AppTheme
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import io.ktor.client.statement.request
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import java.util.Locale.getDefault
class ChatEditActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val chatId = intent.getLongExtra("chatId", 0L)
setContent {
val chats by ChatManager.chatsList.collectAsState()
var selectedImages by remember {
mutableStateOf<List<Uri?>>(emptyList())
}
val editingChat = chats[chatId]
val avatarUrl = if (selectedImages.isNotEmpty()) {
selectedImages.last().toString()
} else {
editingChat?.avatarUrl
}
val title = remember { mutableStateOf(editingChat?.title) }
val description = remember { mutableStateOf(editingChat?.description) }
val context = LocalContext.current
val density = LocalDensity.current
val isShowButton = title.value != editingChat?.title || description.value != editingChat?.description || selectedImages.isNotEmpty()
val coroutineScope = rememberCoroutineScope()
val singlePhotoPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
println("uris $uri")
selectedImages = listOf(uri)
}
)
AppTheme() {
LazyColumn(modifier = Modifier
.fillMaxSize()
.background(colorScheme.background)
.windowInsetsPadding(WindowInsets.statusBars)
) {
item {
IconButton({
finish()
}) {
Icon(
Icons.AutoMirrored.Filled.ArrowBackIos,
"",
modifier = Modifier.size(25.dp),
tint = colorScheme.primary
)
}
}
item {
Column(verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Box(modifier = Modifier.clickable {
singlePhotoPickerLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}, contentAlignment = Alignment.Center) {
if (avatarUrl?.isNotEmpty() ?: false) {
AsyncImage(
avatarUrl,
"",
modifier = Modifier.size(110.dp)
.clip(CircleShape),
contentScale = ContentScale.FillBounds
)
} else {
val initial =
title.value?.split(" ")?.mapNotNull { it.firstOrNull() }
?.take(2)?.joinToString("")?.uppercase(getDefault())
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(110.dp)
.clip(CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(
Utils.getColorForAvatar(title.value!!).first,
Utils.getColorForAvatar(title.value!!).second
)
)
),
) {
Text(
text = initial!!,
color = Color.White,
style = MaterialTheme.typography.labelLarge,
fontSize = 22.sp
)
}
}
Box(
modifier = Modifier
.background(
colorScheme.primaryContainer,
shape = RoundedCornerShape(8.dp)
)
.align(Alignment.BottomEnd),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Filled.PhotoCamera,
contentDescription = "Меню",
modifier = Modifier
.size(30.dp)
.align(Alignment.Center),
tint = colorScheme.onPrimaryContainer
)
}
}
OutlinedTextField(
value = title.value!!,
onValueChange = { newText ->
title.value = newText
},
label = { Text("Название чата") },
textStyle = TextStyle(fontSize = 25.sp),
)
OutlinedTextField(
value = description.value!!,
onValueChange = { newText ->
description.value = newText
},
label = { Text("Описание") },
textStyle = TextStyle(fontSize = 25.sp),
)
AnimatedVisibility(visible = isShowButton, enter = fadeIn(), exit = fadeOut()) {
Button(onClick = {
if (selectedImages.isNotEmpty()) {
// Uhm i think i need another func for it
var uploadedImages = mapOf<String, JsonElement>()
var imageType = ""
var imageName = ""
val cursor = context.contentResolver.query(
selectedImages.last()!!, null, null, null, null
)
cursor?.use {
if (it.moveToFirst()) {
val nameIndex =
it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
imageName = it.getString(nameIndex)
}
}
val packet = SocketManager.packPacket(
OPCode.UPLOAD_IMAGE.opcode, JsonObject(
mapOf(
"count" to JsonPrimitive(1)
)
)
)
val client = HttpClient(CIO)
runBlocking {
val imageBytes = try {
context.contentResolver.openInputStream(selectedImages.last()!!)
?.use { inputStream ->
inputStream.readBytes()
}
} catch (e: Exception) {
null
}
SocketManager.sendPacket(packet, { packet ->
if (packet.payload is JsonObject) {
runBlocking {
try {
val response: HttpResponse =
client.post(packet.payload["url"]?.jsonPrimitive?.content.toString()) {
method = HttpMethod.Post
headers {
append(
HttpHeaders.UserAgent,
"OKMessages/25.12.1 (Android 14; oneplus CPH2465; 382dpi 2300x1023)"
)
append(
HttpHeaders.ContentType,
"application/octet-stream"
)
append(
HttpHeaders.ContentDisposition,
"attachment; filename=${imageName}"
)
append(
"X-Uploading-Mode",
"parallel"
)
append(
"Content-Range",
"bytes 0-${imageBytes!!.size - 1}/${imageBytes.size}"
)
append(
HttpHeaders.Connection,
"keep-alive"
)
append(
HttpHeaders.AcceptEncoding,
"gzip"
)
}
setBody(imageBytes)
}
println(response.request.content)
println("Upload response status: ${response.status}")
val content =
Json.parseToJsonElement(response.bodyAsText())
uploadedImages =
content.jsonObject["photos"]!!.jsonObject
print(content)
var packetJson = mutableMapOf(
"chatId" to JsonPrimitive(chatId),
"photoToken" to JsonPrimitive(uploadedImages.toList()
.last().second.jsonObject["token"]!!.jsonPrimitive.content),
"theme" to JsonPrimitive(title.value),
"description" to JsonPrimitive(description.value)
)
val packet = SocketManager.packPacket(
OPCode.EDIT_CHAT_INFO.opcode,
JsonObject(packetJson)
)
GlobalScope.launch {
SocketManager.sendPacket(
packet, { packet ->
if (packet.payload is JsonObject) {
AccountManager.accountID =
packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["id"]!!.jsonPrimitive.long
AccountManager.phone =
packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["phone"]!!.jsonPrimitive.content
}
})
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
client.close()
selectedImages = emptyList()
}
}
}
})
}
} else {
val packet = SocketManager.packPacket(OPCode.EDIT_CHAT_INFO.opcode,
JsonObject(mapOf(
"chatId" to JsonPrimitive(chatId),
"theme" to JsonPrimitive(title.value),
"description" to JsonPrimitive(description.value)
))
)
coroutineScope.launch {
SocketManager.sendPacket(packet, { packet ->
if (packet.payload is JsonObject) {
GlobalScope.launch {
ChatManager.processSingleChat(packet.payload["chat"]!!.jsonObject)
}
}
})
}
}
}, modifier = Modifier.align(Alignment.CenterHorizontally)) {
Text("Сохранить", fontSize = 18.sp)
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,750 @@
package com.sffteam.voidclient
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Reply
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
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.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.sffteam.voidclient.preferences.SettingsActivity
import com.sffteam.voidclient.ui.theme.AppTheme
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone
import kotlinx.datetime.number
import kotlinx.datetime.toJavaDayOfWeek
import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import java.time.Duration
import java.time.format.TextStyle
import java.util.Date
import java.util.Locale.getDefault
import kotlin.collections.get
import kotlin.time.ExperimentalTime
import kotlin.time.Instant.Companion.fromEpochMilliseconds
class ChatListActivity : ComponentActivity() {
@OptIn(DelicateCoroutinesApi::class, ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent @OptIn(ExperimentalMaterial3Api::class) {
Utils.windowSize = calculateWindowSizeClass(this)
AppTheme {
DrawChatList()
}
}
}
}
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class, DelicateCoroutinesApi::class)
@Composable
fun DrawChatList() {
val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) }
var showPopup by remember { mutableStateOf(false) }
if (showPopup) {
var inputText by remember { mutableStateOf("") }
var errorText by remember { mutableStateOf("") }
AlertDialog(title = {
Text(text = "Войти в группу")
}, text = {
Column() {
OutlinedTextField(
value = inputText,
onValueChange = { inputText = it },
label = { Text("Введите ссылку на группу") },
singleLine = true
)
Text(
errorText
)
}
}, onDismissRequest = {
showPopup = false
}, confirmButton = {
TextButton(
onClick = {
val urlJoin = inputText.replace("https://max.ru/", "")
val packetSend = SocketManager.packPacket(
OPCode.JOIN_CHAT.opcode, JsonObject(
mapOf(
"link" to JsonPrimitive(urlJoin)
)
)
)
GlobalScope.launch {
SocketManager.sendPacket(packetSend, callback = { packet ->
if (packet.payload is JsonObject) {
if ("error" in packet.payload) {
errorText = packet.payload["localizedMessage"].toString()
} else {
GlobalScope.launch {
ChatManager.processChats(JsonArray(listOf(packet.payload["chat"]?.jsonObject) as List<JsonElement>))
}
showPopup = false
showBottomSheet = false
}
}
})
}
}) {
Text("Войти", fontSize = 20.sp)
}
}, dismissButton = {
TextButton(
onClick = {
showPopup = false
}) {
Text("Отмена", fontSize = 20.sp)
}
})
}
if (showBottomSheet) {
ModalBottomSheet(
onDismissRequest = {
showBottomSheet = false
}, sheetState = sheetState
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(start = 8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
showPopup = true
}) {
Icon(
Icons.Filled.Add,
contentDescription = "edit message",
modifier = Modifier
.padding(end = 10.dp)
.size(20.dp)
.align(Alignment.CenterVertically)
)
Text(
text = "Войти в группу",
fontSize = 25.sp,
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
}
}
Column {
val titleSize = when (Utils.windowSize.widthSizeClass) {
WindowWidthSizeClass.Compact -> 18.sp
WindowWidthSizeClass.Medium -> 24.sp
WindowWidthSizeClass.Expanded -> 28.sp
else -> 24.sp
}
val chats by ChatManager.chatsList.collectAsState()
val listState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
LaunchedEffect(chats) {
coroutineScope.launch {
listState.scrollToItem(index = chats.size)
}
}
Scaffold(topBar = {
CenterAlignedTopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = colorScheme.surfaceContainer,
), title = {
Text(
"Void Client",
fontSize = titleSize,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold
)
}, navigationIcon = {
IconButton({
val intent = Intent(context, SettingsActivity::class.java)
context.startActivity(intent)
}) {
Icon(
Icons.Filled.Settings, contentDescription = "Меню"
)
}
}, actions = {
IconButton({ showBottomSheet = true }) {
Icon(
Icons.Filled.Add, contentDescription = "Добавить чат"
)
}
IconButton({ }) { Icon(Icons.Filled.Search, contentDescription = "Поиск") }
}, modifier = Modifier.heightIn(max = 200.dp)
)
}) {
LazyColumn(reverseLayout = true, state = listState, modifier = Modifier.padding(it)) {
items(chats.entries.toList().sortedBy { (_, value) ->
value.messages.entries.toList()
.maxByOrNull { (_, value) -> value.sendTime }!!.value.sendTime
}, key = { entry ->
entry.key
}) { entry ->
println(entry)
DrawUser(entry.key, entry.value, LocalContext.current, Modifier.weight(0.5f))
}
}
}
}
}
@OptIn(ExperimentalTime::class)
@Composable
fun DrawUser(chatID: Long, chat: Chat, context: Context, modifier: Modifier) {
var chatTitle: String
var chatIcon: String
val users by UserManager.usersList.collectAsState()
var secondUser = 0L
val sortedMessages = chat.messages.entries.toList().sortedBy { (_, value) -> value.sendTime }
val lastMessage = sortedMessages.last().value
var lastMsgUser = ""
if (chat.type == "CHAT") {
if (lastMessage.senderID == AccountManager.accountID) {
lastMsgUser = "Вы"
} else {
lastMsgUser =
users[lastMessage.senderID]?.firstName.toString()
if (users[lastMessage.senderID]?.lastName?.isNotEmpty()
?: false
) {
lastMsgUser += " " + users[lastMessage.senderID]?.lastName
}
}
}
if (chat.type == "DIALOG" && chatID != 0L) {
for (i in chat.users.toList()) {
if (i.first != AccountManager.accountID) {
secondUser = i.first
break
}
}
val user = users[secondUser]
chatTitle = user?.firstName + " " + user?.lastName
chatIcon = user?.avatarUrl.toString()
} else {
chatTitle = chat.title
chatIcon = chat.avatarUrl
}
Box(
modifier = modifier
.height(80.dp)
.fillMaxWidth()
.clickable {
val intent = Intent(context, ChatActivity::class.java)
intent.putExtra("chatTitle", chatTitle)
intent.putExtra("chatIcon", chatIcon)
intent.putExtra("chatID", chatID)
intent.putExtra("messageTime", lastMessage.sendTime)
intent.putExtra("chatType", chat.type)
context.startActivity(intent)
}
.background(Color.Transparent)
.padding(start = 12.dp, end = 16.dp),
contentAlignment = Alignment.Center) {
val fontTitleSize = when (Utils.windowSize.widthSizeClass) {
WindowWidthSizeClass.Compact -> 20.sp
WindowWidthSizeClass.Medium -> 24.sp
WindowWidthSizeClass.Expanded -> 30.sp
else -> 24.sp
}
val fontSize = when (Utils.windowSize.widthSizeClass) {
WindowWidthSizeClass.Compact -> 18.sp
WindowWidthSizeClass.Medium -> 24.sp
WindowWidthSizeClass.Expanded -> 28.sp
else -> 24.sp
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp)
) {
if (chatIcon.isNotEmpty()) {
AsyncImage(
model = chatIcon,
contentDescription = "ChatIcon",
modifier = Modifier
.width(60.dp)
.height(60.dp)
.clip(CircleShape),
contentScale = ContentScale.FillBounds
)
} else if (chatID == 0L) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.width(60.dp)
.height(60.dp)
.clip(CircleShape)
.background(colorScheme.primaryContainer),
) {
Icon(
Icons.Filled.Bookmark,
contentDescription = "edit message",
modifier = Modifier
.size(30.dp)
.align(Alignment.Center),
tint = colorScheme.onPrimaryContainer
)
}
} else {
val initial =
chatTitle.split(" ").mapNotNull { it.firstOrNull() }.take(2).joinToString("")
.uppercase(getDefault())
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.width(60.dp)
.height(60.dp)
.clip(CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(
Utils.getColorForAvatar(chatTitle).first,
Utils.getColorForAvatar(chatTitle).second
)
)
),
) {
Text(
text = initial,
color = Color.White,
style = MaterialTheme.typography.labelLarge,
fontSize = 25.sp
)
}
}
Column() {
Row(modifier = Modifier.fillMaxWidth()) {
val lastMessageTime = lastMessage.sendTime
val currentTime = Date().time
val instantLast = fromEpochMilliseconds(lastMessageTime)
val duration = Duration.ofSeconds(currentTime / 1000 - lastMessageTime / 1000)
val localDateTime = instantLast.toLocalDateTime(TimeZone.currentSystemDefault())
val hours = if (localDateTime.hour < 10) {
"0${localDateTime.hour}"
} else {
localDateTime.hour
}
val minutes = if (localDateTime.minute < 10) {
"0${localDateTime.minute}"
} else {
localDateTime.minute
}
val dayOfWeek = localDateTime.dayOfWeek.toJavaDayOfWeek().getDisplayName(
TextStyle.SHORT, getDefault()
)
val time = if (duration.toHours() < 24) {
"${hours}:${minutes}"
} else if (duration.toHours() >= 24 && duration.toDays() < 7) {
dayOfWeek.toString().replaceFirstChar { it.titlecase(getDefault()) }
} else {
"${localDateTime.day}.${localDateTime.month.number}.${localDateTime.year}"
}
Text(
chatTitle,
fontSize = fontTitleSize,
textAlign = TextAlign.Start,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
fontWeight = FontWeight.Bold,
modifier = Modifier
.weight(0.45f)
.padding(end = 4.dp)
)
if (lastMessage.message.isNotEmpty() || lastMessage.attaches != JsonNull) {
Text(
text = time, fontSize = 16.sp, modifier = Modifier.alpha(0.7f)
)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp)
) {
if (lastMessage.attaches is JsonArray && !(lastMessage.attaches?.jsonArray?.isNotEmpty() == true && lastMessage.attaches.jsonArray?.last()?.jsonObject?.contains(
"event"
) == true)
) {
if (chat.type == "CHAT") {
lastMsgUser += ": "
}
val lastMSGCleared = if (lastMessage.link.type == "FORWARD") {
lastMessage.link.msgForLink.message.replace("\n", " ")
} else {
lastMessage.message.replace("\n", " ")
}
val annotatedString = buildAnnotatedString {
append(lastMsgUser)
if (lastMessage.attaches?.jsonArray?.isNotEmpty() ?: false) {
lastMessage.attaches.jsonArray.forEachIndexed { index, jsonelement ->
val type =
jsonelement.jsonObject["_type"]!!.jsonPrimitive.content
if (type == "PHOTO") {
val imageId = "image_$index"
appendInlineContent(id = imageId)
append(" ")
}
}
}
if (lastMessage.link.msgForLink.attaches is JsonArray && lastMessage.link.type == "FORWARD") {
lastMessage.link.msgForLink.attaches.jsonArray.forEachIndexed { index, jsonelement ->
val type =
jsonelement.jsonObject["_type"]!!.jsonPrimitive.content
if (type == "PHOTO") {
val imageId = "image_$index"
appendInlineContent(id = imageId)
append(" ")
}
}
}
append(lastMSGCleared)
if (lastMessage.link.type == "FORWARD") {
appendInlineContent(id = "iconId")
}
}
val inlineContentMap = mutableMapOf<String, InlineTextContent>(
)
val placeholder = Placeholder(
width = 25.sp,
height = 25.sp,
placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter
)
if (lastMessage.attaches?.jsonArray?.isNotEmpty() ?: false) {
lastMessage.attaches.jsonArray.forEachIndexed { index, jsonelement ->
val type = jsonelement.jsonObject["_type"]!!.jsonPrimitive.content
if (type == "PHOTO") {
val imageId = "image_$index"
inlineContentMap[imageId] =
InlineTextContent(placeholder) { _ ->
AsyncImage(
model = jsonelement.jsonObject["baseUrl"]!!.jsonPrimitive.content,
contentDescription = "ChatIcon",
modifier = Modifier
.size(25.dp)
.clip(RoundedCornerShape(2.dp)),
contentScale = ContentScale.Crop
)
}
}
}
}
if (lastMessage.link.type == "FORWARD" && lastMessage.link.msgForLink.attaches is JsonArray) {
lastMessage.link.msgForLink.attaches.jsonArray.forEachIndexed { index, jsonelement ->
println("sh1t jsonelm $jsonelement")
val type = jsonelement.jsonObject["_type"]!!.jsonPrimitive.content
if (type == "PHOTO") {
val imageId = "image_$index"
inlineContentMap[imageId] =
InlineTextContent(placeholder) { _ ->
AsyncImage(
model = jsonelement.jsonObject["baseUrl"]!!.jsonPrimitive.content,
contentDescription = "ChatIcon",
modifier = Modifier
.size(25.dp)
.clip(RoundedCornerShape(2.dp)),
contentScale = ContentScale.Crop
)
}
}
}
}
if (lastMessage.link.type == "FORWARD") {
inlineContentMap["iconId"] = InlineTextContent(placeholder) { _ ->
Icon(
imageVector = Icons.AutoMirrored.Filled.Reply,
contentDescription = "forwardIcon",
)
}
}
Text(
text = annotatedString,
fontSize = fontSize,
inlineContent = inlineContentMap,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.alpha(0.7f)
)
} else {
var text = ""
if (lastMessage.attaches != JsonNull) {
val attach = lastMessage.attaches?.jsonArray?.last()
val event = attach?.jsonObject["event"]?.jsonPrimitive?.content
when (event) {
"remove" -> {
val peoplesRemoved =
attach?.jsonObject["userId"]?.jsonPrimitive?.long
UserManager.checkForExisting(peoplesRemoved!!)
UserManager.checkForExisting(lastMessage.senderID)
var whomAdded = users[peoplesRemoved]?.firstName.toString()
if (users[peoplesRemoved]?.lastName?.isNotEmpty() == true) {
whomAdded += " " + users[peoplesRemoved]?.lastName
}
if (lastMessage.senderID == AccountManager.accountID) {
text += "Вы удалили $whomAdded"
} else {
var whoAdded =
users[lastMessage.senderID]?.firstName.toString()
if (users[lastMessage.senderID]?.firstName?.isNotEmpty() == true) {
whoAdded += " " + users[lastMessage.senderID]?.lastName
}
text += "$whoAdded удалил(-а) $whomAdded"
}
}
"new" -> {
UserManager.checkForExisting(lastMessage.senderID)
if (chat.type != "CHANNEL") {
if (lastMessage.senderID == AccountManager.accountID) {
text += "$lastMsgUser создали чат"
} else {
text += "$lastMsgUser создал(-а) чат"
}
} else {
text += "Канал создан"
}
}
"add" -> {
val peoplesAdded = attach.jsonObject["userIds"]?.jsonArray
for (i in peoplesAdded!!) {
if (attach.jsonObject["userIds"]?.jsonArray?.isNotEmpty() == true) {
UserManager.checkForExisting(i.jsonPrimitive.long)
}
}
UserManager.checkForExisting(lastMessage.senderID)
if (lastMessage.senderID == AccountManager.accountID) {
text += "Вы добавили "
} else {
var whoAdded =
users[lastMessage.senderID]?.firstName.toString()
if (users[lastMessage.senderID]?.firstName?.isNotEmpty() == true) {
whoAdded += " " + users[lastMessage.senderID]?.lastName
}
text += "$whoAdded добавил(-а) "
}
for (i in peoplesAdded) {
var whomAdded =
users[i.jsonPrimitive.long]?.firstName.toString()
if (users[i.jsonPrimitive.long]?.lastName?.isNotEmpty() == true) {
whomAdded += " " + users[i.jsonPrimitive.long]?.lastName
}
text += whomAdded
if (i != peoplesAdded.last()) {
text += ", "
}
}
}
"icon" -> {
UserManager.checkForExisting(lastMessage.senderID)
if (lastMessage.senderID == AccountManager.accountID) {
text += "$lastMsgUser изменили фото чата"
} else {
text += "$lastMsgUser изменил(-а) фото чата"
}
}
"title" -> {
UserManager.checkForExisting(lastMessage.senderID)
val newTitle =
attach.jsonObject["title"]?.jsonPrimitive?.content
if (lastMessage.senderID == AccountManager.accountID) {
text += "$lastMsgUser изменили название чата на «$newTitle»"
} else {
text += "$lastMsgUser изменил(-а) название чата на «$newTitle»"
}
}
"leave" -> {
UserManager.checkForExisting(lastMessage.senderID)
if (lastMessage.senderID == AccountManager.accountID) {
text += "Вы покинули чат"
} else {
text += "$lastMsgUser покинул(-а) чат"
}
}
"joinByLink" -> {
UserManager.checkForExisting(attach.jsonObject["userId"]?.jsonPrimitive?.long!!)
if (lastMessage.senderID == AccountManager.accountID) {
text += "Вы присоединились к чату"
} else {
text += "$lastMsgUser присоединился(-ась) к чату"
}
}
"system" -> {
text += attach.jsonObject["message"]?.jsonPrimitive?.content
}
}
Text(
text = text,
fontSize = fontSize,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.alpha(0.7f)
)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,641 @@
package com.sffteam.voidclient
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import kotlin.collections.plus
data class msgForLink(
val message: String = "",
val sendTime: Long = 0L,
val senderID: Long = 0L,
val attaches: JsonElement? = JsonArray(emptyList()),
val status: String = "",
val msgID: String = ""
)
data class MessageLink(
val msgForLink: msgForLink = msgForLink(), val type: String = ""
)
data class Message(
val message: String = "",
val sendTime: Long = 0L,
val senderID: Long = 0L,
val attaches: JsonElement? = JsonArray(emptyList()),
val status: String = "",
val link: MessageLink = MessageLink()
)
data class Chat(
val avatarUrl: String,
val title: String,
val messages: Map<String, Message>,
val type: String,
val users: Map<Long, Long>,
val usersCount: Int,
val needGetMessages: Boolean = true,
val description: String = "",
val admins : List<Long> = emptyList(),
val owner : Long = 0L,
val inviteLink : String = "",
val pinned : Int = 0,
)
object ChatManager {
private val _chatsList = MutableStateFlow<Map<Long, Chat>>(emptyMap())
var chatsList = _chatsList.asStateFlow()
fun clearChatsList() {
_chatsList.update {
emptyMap()
}
}
fun removeMessage(chatID: Long, messageID: String) {
_chatsList.update { oldMap ->
oldMap + (chatID to Chat(
oldMap[chatID]?.avatarUrl ?: "",
oldMap[chatID]?.title ?: "",
oldMap[chatID]?.messages?.minus(messageID) ?: emptyMap(),
oldMap[chatID]?.type ?: "",
oldMap[chatID]?.users ?: emptyMap(),
oldMap[chatID]?.usersCount ?: 0,
oldMap[chatID]?.needGetMessages ?: false,
oldMap[chatID]?.description ?: "",
oldMap[chatID]?.admins ?: emptyList(),
oldMap[chatID]?.owner ?: 0L,
oldMap[chatID]?.inviteLink ?: "",
))
}
}
fun addMessage(messageID: String, message: Message, chatID: Long) {
try {
_chatsList.update { oldMap ->
oldMap + (chatID to Chat(
oldMap[chatID]?.avatarUrl ?: "",
oldMap[chatID]?.title ?: "",
oldMap[chatID]?.messages?.plus(mapOf(messageID to message)) ?: emptyMap(),
oldMap[chatID]?.type ?: "",
oldMap[chatID]?.users ?: emptyMap(),
oldMap[chatID]?.usersCount ?: 0,
oldMap[chatID]?.needGetMessages ?: false,
oldMap[chatID]?.description ?: "",
oldMap[chatID]?.admins ?: emptyList(),
oldMap[chatID]?.owner ?: 0L,
oldMap[chatID]?.inviteLink ?: "",
))
}
} catch (e: Exception) {
println(e)
}
}
fun processMessages(messages: JsonArray, chatID: Long) {
val msgList: MutableMap<String, Message> = mutableMapOf()
for (i in messages) {
var message = Message()
var msg = ""
var sendtime = 0L
var senderID = 0L
var attachs = JsonArray(emptyList())
var status = ""
var msgID = ""
var desc = ""
var textForwarded = ""
var senderForwarded = 0L
var msgForwardedID = ""
var forwardedAttaches: JsonElement? = JsonNull
var forwardedType = ""
try {
msg = i.jsonObject["text"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
}
try {
sendtime = i.jsonObject["time"]!!.jsonPrimitive.long
} catch (e: Exception) {
println(e)
}
try {
senderID = i.jsonObject["sender"]!!.jsonPrimitive.long
} catch (e: Exception) {
println(e)
}
try {
attachs = i.jsonObject["attaches"]!!.jsonArray
} catch (e: Exception) {
println(e)
}
try {
status = i.jsonObject["status"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
}
try {
msgID = i.jsonObject["id"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
}
if (i.jsonObject.contains("link")) {
try {
val msgLink = i.jsonObject["link"]
forwardedType = msgLink!!.jsonObject["type"]?.jsonPrimitive!!.content
textForwarded =
msgLink.jsonObject["message"]!!.jsonObject["text"]!!.jsonPrimitive.content
senderForwarded =
msgLink.jsonObject["message"]!!.jsonObject["sender"]!!.jsonPrimitive.long
msgForwardedID =
msgLink.jsonObject["message"]!!.jsonObject["id"]!!.jsonPrimitive.content
forwardedAttaches = msgLink.jsonObject["message"]!!.jsonObject["attaches"]
} catch (e: Exception) {
println(e)
}
}
message = message.copy(
message = msg,
sendTime = sendtime,
senderID = senderID,
attaches = attachs,
status = status,
link = MessageLink(
type = forwardedType, msgForLink = msgForLink(
message = textForwarded,
senderID = senderForwarded,
attaches = forwardedAttaches,
msgID = msgForwardedID,
)
),
)
println("coolmsg $message")
msgList[msgID] = message
}
println("notcool size=${msgList.size}")
_chatsList.update { oldMap ->
oldMap + (chatID to Chat(
oldMap[chatID]?.avatarUrl ?: "",
oldMap[chatID]?.title ?: "",
oldMap[chatID]?.messages?.plus(msgList) ?: emptyMap(),
oldMap[chatID]?.type ?: "",
oldMap[chatID]?.users ?: emptyMap(),
oldMap[chatID]?.usersCount ?: 0,
if (msgList.size == 30) true else false,
oldMap[chatID]?.description ?: "",
oldMap[chatID]?.admins ?: emptyList(),
oldMap[chatID]?.owner ?: 0L,
oldMap[chatID]?.inviteLink ?: "",
))
}
println(_chatsList.value[chatID]?.messages?.size)
}
suspend fun removeChat(chatId : Long) {
val updatedChatsList = _chatsList.value.toMutableMap()
updatedChatsList.remove(chatId)
_chatsList.update {
updatedChatsList
}
}
suspend fun processSingleChat(chat : JsonObject) {
var chatID: Long = 0
try {
chatID = chat.jsonObject["id"]!!.jsonPrimitive.long
} catch (e: Exception) {
println(e)
}
println(chatID)
try {
println("chat $chat")
var lastmsgtm = 0L
var msgID = ""
var lastmsg = ""
var avatarUrl = ""
var status = ""
var title = ""
var senderID = 0L
var type = ""
var users = mutableMapOf<Long, Long>()
var attaches: JsonElement? = JsonNull
var usersCount = 0
var desc = ""
var owner = 0L
var admins: MutableList<Long> = mutableListOf()
var inviteLink = ""
var textForwarded = ""
var senderForwarded = 0L
var msgForwardedID = ""
var forwardedAttaches: JsonElement? = JsonNull
var forwardedType = ""
try {
lastmsgtm =
chat.jsonObject["lastMessage"]!!.jsonObject["time"]!!.jsonPrimitive.long
} catch (e: Exception) {
println(e)
}
try {
lastmsg =
chat.jsonObject["lastMessage"]!!.jsonObject["text"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
}
try {
senderID =
chat.jsonObject["lastMessage"]!!.jsonObject["sender"]!!.jsonPrimitive.long
} catch (e: Exception) {
println(e)
}
try {
status =
chat.jsonObject["lastMessage"]!!.jsonObject["status"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
}
try {
msgID = chat.jsonObject["lastMessage"]!!.jsonObject["id"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
}
if (chat.jsonObject["lastMessage"]!!.jsonObject.contains("attaches")) {
try {
attaches = chat.jsonObject["lastMessage"]!!.jsonObject["attaches"]
} catch (e: Exception) {
println(e)
}
} else {
attaches = JsonArray(emptyList())
}
if (chat.jsonObject["lastMessage"]!!.jsonObject.contains("link")) {
try {
val msgLink = chat.jsonObject["lastMessage"]!!.jsonObject["link"]
forwardedType = msgLink!!.jsonObject["type"]?.jsonPrimitive!!.content
textForwarded =
msgLink.jsonObject["message"]!!.jsonObject["text"]!!.jsonPrimitive.content
senderForwarded =
msgLink.jsonObject["message"]!!.jsonObject["sender"]!!.jsonPrimitive.long
msgForwardedID =
msgLink.jsonObject["message"]!!.jsonObject["id"]!!.jsonPrimitive.content
forwardedAttaches = msgLink.jsonObject["message"]!!.jsonObject["attaches"]
} catch (e: Exception) {
println(e)
}
}
if (chat.jsonObject.contains("baseIconUrl")) {
try {
avatarUrl = chat.jsonObject["baseIconUrl"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
}
}
if (chat.jsonObject.contains("title")) {
try {
title = chat.jsonObject["title"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
}
} else {
if (chatID == 0L) {
title = "Избранное"
}
}
try {
type = chat.jsonObject["type"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
}
try {
for (i in chat.jsonObject["participants"]?.jsonObject?.toList()!!) {
users[i.first.toLong()] = i.second.jsonPrimitive.long
}
} catch (e: Exception) {
println(e)
}
if (chat.jsonObject.contains("participantsCount")) {
try {
usersCount = chat.jsonObject["participantsCount"]!!.jsonPrimitive.int
} catch (e: Exception) {
println(e)
}
}
if (chat.jsonObject.contains("description")) {
desc = chat.jsonObject["description"]?.jsonPrimitive?.content!!
}
if (chat.jsonObject.contains("owner")) {
owner = chat.jsonObject["owner"]?.jsonPrimitive?.long!!
}
if (chat.jsonObject.contains("admins")) {
for (y in chat.jsonObject["admins"]?.jsonArray?.toList()!!) {
admins += y.jsonPrimitive.long
}
}
if (chat.jsonObject.contains("link")) {
inviteLink = chat.jsonObject["link"]?.jsonPrimitive?.content!!
}
val messages: Map<String, Message> = mapOf(
msgID to Message(
lastmsg, lastmsgtm, senderID, attaches, status, link = MessageLink(
type = forwardedType, msgForLink = msgForLink(
textForwarded,
senderID = senderForwarded,
attaches = forwardedAttaches,
msgID = msgForwardedID
)
)
)
)
_chatsList.update { oldMap ->
oldMap.toMap() + (chatID to Chat(
avatarUrl,
title,
if (oldMap[chatID]?.messages?.isNotEmpty() == true) oldMap[chatID]?.messages?.plus(
messages
)!! else messages,
type,
users,
usersCount,
oldMap[chatID]?.needGetMessages ?: true,
desc,
admins,
owner,
inviteLink,
))
}
} catch (e : Exception) {
println(e)
}
}
/* Function. */
suspend fun processChats(chats: JsonArray): Boolean {
val userIds = mutableListOf<JsonElement>()
for (i in chats) {
var chatID: Long = 0
try {
chatID = i.jsonObject["id"]!!.jsonPrimitive.long
} catch (e: Exception) {
println(e)
}
println(chatID)
try {
println("chat $i")
var lastmsgtm = 0L
var msgID = ""
var lastmsg = ""
var avatarUrl = ""
var status = ""
var title = ""
var senderID = 0L
var type = ""
var users = mutableMapOf<Long, Long>()
var attaches: JsonElement? = JsonNull
var usersCount = 0
var desc = ""
var owner = 0L
var admins : MutableList<Long> = mutableListOf()
var inviteLink = ""
var textForwarded = ""
var senderForwarded = 0L
var msgForwardedID = ""
var forwardedAttaches: JsonElement? = JsonNull
var forwardedType = ""
if (i.jsonObject.contains("lastMessage")) {
try {
lastmsgtm =
i.jsonObject["lastMessage"]!!.jsonObject["time"]!!.jsonPrimitive.long
} catch (e: Exception) {
println(e)
}
try {
lastmsg =
i.jsonObject["lastMessage"]!!.jsonObject["text"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
}
try {
senderID =
i.jsonObject["lastMessage"]!!.jsonObject["sender"]!!.jsonPrimitive.long
} catch (e: Exception) {
println(e)
}
try {
status =
i.jsonObject["lastMessage"]!!.jsonObject["status"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
}
try {
msgID = i.jsonObject["lastMessage"]!!.jsonObject["id"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
}
if (i.jsonObject["lastMessage"]!!.jsonObject.contains("attaches")) {
try {
attaches = i.jsonObject["lastMessage"]!!.jsonObject["attaches"]
} catch (e: Exception) {
println(e)
}
} else {
attaches = JsonArray(emptyList())
}
if (i.jsonObject["lastMessage"]!!.jsonObject.contains("link")) {
try {
val msgLink = i.jsonObject["lastMessage"]!!.jsonObject["link"]
forwardedType = msgLink!!.jsonObject["type"]?.jsonPrimitive!!.content
textForwarded =
msgLink.jsonObject["message"]!!.jsonObject["text"]!!.jsonPrimitive.content
senderForwarded =
msgLink.jsonObject["message"]!!.jsonObject["sender"]!!.jsonPrimitive.long
msgForwardedID =
msgLink.jsonObject["message"]!!.jsonObject["id"]!!.jsonPrimitive.content
forwardedAttaches = msgLink.jsonObject["message"]!!.jsonObject["attaches"]
} catch (e: Exception) {
println(e)
}
}
}
if (i.jsonObject.contains("baseIconUrl")) {
try {
avatarUrl = i.jsonObject["baseIconUrl"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
}
}
if (i.jsonObject.contains("title")) {
try {
title = i.jsonObject["title"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
}
} else {
if (chatID == 0L) {
title = "Избранное"
}
}
try {
type = i.jsonObject["type"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
}
print("pediki ${i.jsonObject["participants"]}")
try {
for (i in i.jsonObject["participants"]?.jsonObject?.toList()!!) {
println("partip ${i}")
userIds += Json.encodeToJsonElement(Long.serializer(), i.first.toLong())
users[i.first.toLong()] = i.second.jsonPrimitive.long
}
} catch (e: Exception) {
println("pizda")
println(e)
}
if (i.jsonObject.contains("participantsCount")) {
try {
usersCount = i.jsonObject["participantsCount"]!!.jsonPrimitive.int
} catch (e: Exception) {
println(e)
}
}
if (i.jsonObject.contains("description")) {
desc = i.jsonObject["description"]?.jsonPrimitive?.content!!
}
if (i.jsonObject.contains("owner")) {
owner = i.jsonObject["owner"]?.jsonPrimitive?.long!!
}
if (i.jsonObject.contains("admins")) {
for (y in i.jsonObject["admins"]?.jsonArray?.toList()!!) {
admins += y.jsonPrimitive.long
}
}
if (i.jsonObject.contains("link")) {
inviteLink = i.jsonObject["link"]!!.jsonPrimitive.content
}
val messages: Map<String, Message> = mapOf(
msgID to Message(
lastmsg, lastmsgtm, senderID, attaches, status, link = MessageLink(
type = forwardedType, msgForLink = msgForLink(
textForwarded,
senderID = senderForwarded,
attaches = forwardedAttaches,
msgID = msgForwardedID
)
)
)
)
_chatsList.update { oldMap ->
oldMap.toMap() + (chatID to Chat(
avatarUrl,
title,
if (oldMap[chatID]?.messages?.isNotEmpty() == true) oldMap[chatID]?.messages?.plus(messages)!! else messages,
type,
users,
usersCount,
oldMap[chatID]?.needGetMessages ?: true,
desc,
admins,
owner,
inviteLink,
))
}
println("current chat ${_chatsList.value[chatID]}")
} catch (e: Exception) {
println(e)
}
}
val packet = SocketManager.packPacket(
OPCode.CONTACTS_INFO.opcode, JsonObject(
mapOf(
"contactIds" to JsonArray(userIds),
)
)
)
SocketManager.sendPacket(
packet, { packet ->
println(packet.payload)
if (packet.payload is JsonObject) {
GlobalScope.launch {
UserManager.processUsers(packet.payload["contacts"]!!.jsonArray)
}
}
})
return true
}
}

View File

@@ -0,0 +1,470 @@
package com.sffteam.voidclient
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
import androidx.compose.material.icons.automirrored.filled.ExitToApp
import androidx.compose.material.icons.filled.ArrowBackIos
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.ExitToApp
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import coil3.compose.AsyncImage
import com.sffteam.voidclient.ui.theme.AppTheme
import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import java.util.Locale.getDefault
import kotlin.time.ExperimentalTime
import kotlin.time.Instant.Companion.fromEpochMilliseconds
class ChatViewActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val chatId = intent.getLongExtra("chatId", 0L)
setContent {
val chats by ChatManager.chatsList.collectAsState()
val users by UserManager.usersList.collectAsState()
val viewedChat = chats[chatId]
val usersInChat = if (viewedChat?.users?.isNotEmpty() == true) {
viewedChat.users
} else {
null
}
val description = chats[chatId]?.description
val inviteLink = chats[chatId]?.inviteLink
val owner = chats[chatId]?.owner
val type = viewedChat?.type
val avatarUrl = viewedChat?.avatarUrl
val coroutineScope = rememberCoroutineScope()
val clipboardManager = LocalClipboard.current
val context = LocalContext.current
AppTheme {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(colorScheme.background)
.windowInsetsPadding(WindowInsets.statusBars)
) {
item {
Row() {
IconButton({
finish()
}) {
Icon(
Icons.AutoMirrored.Filled.ArrowBackIos,
"",
modifier = Modifier.size(25.dp),
tint = colorScheme.primary
)
}
if (AccountManager.accountID == owner) {
Spacer(modifier = Modifier.weight(1f))
IconButton({
val intent = Intent(context, ChatEditActivity::class.java)
intent.putExtra("chatId", chatId)
context.startActivity(intent)
}) {
Icon(
Icons.Filled.Edit,
"",
modifier = Modifier.size(25.dp),
tint = colorScheme.primary
)
}
}
}
}
item {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
if (avatarUrl?.isNotEmpty() == true) {
AsyncImage(
avatarUrl,
"",
modifier = Modifier
.align(Alignment.CenterHorizontally)
.size(110.dp)
.clip(CircleShape)
.clickable {
val intent = Intent(context, ImageViewerActivity::class.java)
intent.putExtra("isSingleImage", true)
intent.putExtra("image", avatarUrl)
context.startActivity(intent)
}
)
} else {
val initial =
viewedChat?.title?.split(" ")?.mapNotNull { it.firstOrNull() }
?.take(2)?.joinToString("")?.uppercase(getDefault())
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.width(100.dp)
.height(100.dp)
.clip(CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(
Utils.getColorForAvatar(viewedChat?.title.toString()).first,
Utils.getColorForAvatar(viewedChat?.title.toString()).second
)
)
)
.align(Alignment.CenterHorizontally),
) {
Text(
text = initial.toString(),
color = Color.White,
style = MaterialTheme.typography.labelLarge,
fontSize = 40.sp
)
}
}
Text(
viewedChat?.title.toString(),
fontSize = 24.sp,
modifier = Modifier.align(Alignment.CenterHorizontally).padding(start = 8.dp),
color = colorScheme.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
var desc = ""
when (type) {
"CHAT" -> {
val sizeString = viewedChat.users.size.toString()
val sizeStringLast = sizeString.last().code
desc = when (sizeStringLast) {
1 -> {
"Тут только вы"
}
else -> {
if (sizeStringLast == 2 || sizeStringLast == 3 || sizeStringLast == 4) {
"$sizeString участника"
} else if (sizeStringLast == 1 && sizeString != "11") {
"$sizeString участник"
} else {
"$sizeString участников"
}
}
}
}
"CHANNEL" -> {
desc = if (viewedChat.usersCount.toString()
.last().code == 2 || viewedChat.usersCount.toString()
.last().code == 3 || viewedChat.usersCount.toString()
.last().code == 4
) {
println(
"code ${
viewedChat.usersCount.toString().last().code
}"
)
viewedChat.usersCount.toString() + " подписчика"
} else if (viewedChat.usersCount.toString()
.last().code == 1 && viewedChat.usersCount.toString() != "11"
) {
viewedChat.usersCount.toString() + " подписчик"
} else {
viewedChat.usersCount.toString() + " подписчиков"
}
}
}
Text(
desc,
fontSize = 18.sp,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.alpha(0.7f),
color = colorScheme.secondary
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
modifier = Modifier.padding(start = 8.dp).fillMaxWidth()) {
Button(onClick = {
val packet = SocketManager.packPacket(
OPCode.LEAVE_CHAT.opcode, JsonObject(
mapOf(
"chatId" to JsonPrimitive(chatId)
)
)
)
coroutineScope.launch {
SocketManager.sendPacket(packet, {
})
}
val intent = Intent(context, ChatListActivity::class.java)
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
context.startActivity(intent)
finish()
}, modifier = Modifier.padding(4.dp)) {
Column() {
Icon(
Icons.AutoMirrored.Filled.ExitToApp,
"",
modifier = Modifier.size(25.dp).align(Alignment.CenterHorizontally)
)
Text("Покинуть группу", fontSize = 18.sp, modifier = Modifier.align(Alignment.CenterHorizontally))
}
}
}
}
}
item {
if (description?.isNotEmpty() == true) {
Box(modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.background(colorScheme.secondaryContainer, RoundedCornerShape(16.dp))
) {
Column(modifier = Modifier.padding(8.dp)) {
Text("Описание", modifier = Modifier.alpha(0.7f),
color = colorScheme.onSecondaryContainer, fontSize = 14.sp)
Text(description, color = colorScheme.onSecondaryContainer, fontSize = 18.sp)
}
}
}
}
item {
if (inviteLink?.isNotEmpty() == true) {
Box(modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.background(colorScheme.secondaryContainer, RoundedCornerShape(16.dp))
) {
Column(modifier = Modifier.padding(8.dp)
.clickable {
coroutineScope.launch {
clipboardManager.setClipEntry(
ClipEntry(
ClipData.newPlainText(
inviteLink, inviteLink
)
)
)
}
}) {
Text("Ссылка для приглашения в чат", modifier = Modifier.alpha(0.7f),
color = colorScheme.onSecondaryContainer, fontSize = 14.sp)
Text(inviteLink, color = colorScheme.onSecondaryContainer, fontSize = 18.sp)
}
}
}
}
item {
Box(modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.background(colorScheme.secondaryContainer, RoundedCornerShape(16.dp))
) {
Column(modifier = Modifier.padding(8.dp)) {
Row(modifier = Modifier.padding(bottom = 8.dp)) {
Text("Участники", color = colorScheme.onSecondaryContainer, fontSize = 22.sp)
}
if (usersInChat?.isNotEmpty() == true) {
for (user in usersInChat?.toList()?.sortedByDescending { it.second }!!) {
val userIn = users[user.first]
if (userIn != null) {
DrawUser(userIn, user.first, viewedChat!!.owner, viewedChat.admins, user.second, context)
} else {
UserManager.checkForExisting(user.first)
}
}
}
}
}
}
}
}
}
}
}
@OptIn(ExperimentalTime::class)
@Composable
fun DrawUser(user : User, userId : Long, owner : Long, admins : List<Long>, time : Long, context : Context) {
val fullName = user.firstName + if (user.lastName.isNotEmpty()) {
" " + user.lastName
} else {
""
}
val userRole = if (userId == owner) {
"Владелец"
} else {
var admin = ""
for (i in admins) {
if (userId == i) {
admin = "Администратор"
break
}
}
admin
}
val lastSeen = fromEpochMilliseconds(time)
val localLastSeen =
lastSeen.toLocalDateTime(TimeZone.currentSystemDefault())
Box(modifier = Modifier.padding(bottom = 8.dp).clickable {
val intent = Intent(context, ProfileViewActivity::class.java)
intent.putExtra("userId", userId)
context.startActivity(intent)
}) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)) {
if (user.avatarUrl.isNotEmpty()) {
AsyncImage(
user.avatarUrl,
"",
modifier = Modifier.size(45.dp)
.clip(CircleShape),
contentScale = ContentScale.FillBounds
)
} else {
val initial =
fullName.split(" ").mapNotNull { it.firstOrNull() }
.take(2).joinToString("").uppercase(getDefault())
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.width(45.dp)
.height(45.dp)
.clip(CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(
Utils.getColorForAvatar(fullName).first,
Utils.getColorForAvatar(fullName).second
)
)
),
) {
Text(
text = initial,
color = Color.White,
style = MaterialTheme.typography.labelLarge,
fontSize = 22.sp
)
}
}
Column(verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier.weight(0.5f)) {
Text(fullName,
fontSize = 22.sp,
color = colorScheme.onSecondaryContainer,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text("Был(а) недавно",
fontSize = 16.sp,
color = colorScheme.onSecondaryContainer,
modifier = Modifier.alpha(0.7f)
)
}
if (userRole.isNotEmpty()) {
Text(userRole,
fontSize = 18.sp,
color = colorScheme.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}

View File

@@ -0,0 +1,170 @@
package com.sffteam.voidclient
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.sffteam.voidclient.ui.theme.AppTheme
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "token")
class CodeActivity : ComponentActivity() {
@OptIn(DelicateCoroutinesApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AppTheme {
val code = remember { mutableStateOf("") }
val errorText = remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(colorScheme.background),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = code.value,
onValueChange = { newText -> code.value = newText },
label = { Text("Введите код из СМС") },
textStyle = TextStyle(fontSize = 25.sp),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
)
)
Text(
errorText.value, color = Color.White, fontSize = 25.sp
)
val context = LocalContext.current
Button(
modifier = Modifier.padding(16.dp), onClick = {
val packet = SocketManager.packPacket(
OPCode.CHECK_CODE.opcode, JsonObject(
mapOf(
"token" to JsonPrimitive(
intent.getStringExtra("token").toString()
),
"verifyCode" to JsonPrimitive(code.value),
"authTokenType" to JsonPrimitive("CHECK_CODE")
)
)
)
GlobalScope.launch {
SocketManager.sendPacket(
packet, { packet ->
println(packet.payload)
if (packet.payload is JsonObject) {
if ("error" in packet.payload) {
errorText.value =
packet.payload["localizedMessage"].toString()
} else if ("tokenAttrs" in packet.payload) {
if ("REGISTER" in packet.payload["tokenAttrs"]!!.jsonObject) {
val intent = Intent(
context, RegisterActivity::class.java
)
val token =
packet.payload["tokenAttrs"]!!.jsonObject["REGISTER"]!!.jsonObject["token"]!!.jsonPrimitive.content
intent.putExtra("token", token)
startActivity(intent)
finish()
} else if ("passwordChallenge" in packet.payload) {
val intent = Intent(
context, PasswordCheckActivity::class.java
)
val trackId =
packet.payload["passwordChallenge"]?.jsonObject["trackId"]?.jsonPrimitive?.content
val hint =
packet.payload["passwordChallenge"]?.jsonObject["hint"]?.jsonPrimitive?.content
val email =
packet.payload["passwordChallenge"]?.jsonObject["email"]?.jsonPrimitive?.content
intent.putExtra("trackId", trackId)
intent.putExtra("hint", hint)
intent.putExtra("email", email)
context.startActivity(intent)
finish()
} else {
val intent = Intent(
context, ChatListActivity::class.java
)
runBlocking {
dataStore.edit { settings ->
// Nice sandwich lol
val token =
packet.payload["tokenAttrs"]!!.jsonObject["LOGIN"]!!.jsonObject["token"]!!.jsonPrimitive.content
settings[stringPreferencesKey("token")] =
token
AccountManager.token = token
}
}
GlobalScope.launch {
SocketManager.loginToAccount(context)
}
context.startActivity(intent)
finish()
}
}
}
}
)
}
}) {
Text("Войти", fontSize = 25.sp)
}
}
}
}
}
}

View File

@@ -0,0 +1,247 @@
package com.sffteam.voidclient
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Text
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.sffteam.voidclient.ui.theme.AppTheme
import kotlinx.coroutines.launch
import androidx.core.view.WindowCompat
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.padding
import androidx.core.view.WindowInsetsCompat
class ImageViewerActivity : ComponentActivity() {
@SuppressLint("CoroutineCreationDuringComposition")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val isSingleImage = intent.getBooleanExtra("isSingleImage", false)
WindowCompat.setDecorFitsSystemWindows(window, false)
var pickedPhoto = ""
var chatId = 0L
var url = ""
var scrolled = false
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
if (isSingleImage) {
url = intent.getStringExtra("image").toString()
} else {
chatId = intent.getLongExtra("chatId", 0L)
pickedPhoto = intent.getStringExtra("pickedPhoto").toString()
}
setContent {
var expanded by remember { mutableStateOf(false) }
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
var images: MutableList<Pair<String, Boolean>> = mutableListOf()
var pagerState = rememberPagerState { images.size }
var isTopBar by remember { mutableStateOf(true) }
val interactionSource = remember { MutableInteractionSource() }
if (isTopBar) {
windowInsetsController.show(WindowInsetsCompat.Type.statusBars())
} else {
windowInsetsController.hide(WindowInsetsCompat.Type.statusBars())
}
if (!isSingleImage) {
val chats by ChatManager.chatsList.collectAsState()
val viewedChat = chats[chatId]
for (message in viewedChat!!.messages.toList().sortedByDescending { it.second.sendTime }) {
if (message.second.attaches?.jsonArray?.isNotEmpty() == true) {
println("value $message")
for (attach in message.second.attaches!!.jsonArray) {
if (attach.jsonObject["_type"]?.jsonPrimitive?.content == "PHOTO") {
println(attach.jsonObject["baseUrl"]!!.jsonPrimitive.content)
images.add(attach.jsonObject["baseUrl"]!!.jsonPrimitive.content to false)
val photoToken = attach.jsonObject["photoToken"]!!.jsonPrimitive.content
// i think its cringe code :(
if (pickedPhoto == photoToken && !scrolled) {
coroutineScope.launch {
for (img in 0..images.size) {
if (images[img].first == attach.jsonObject["baseUrl"]!!.jsonPrimitive.content && !images[img].second) {
pagerState.scrollToPage(img)
scrolled = true
break
}
}
}
}
}
}
}
if (message.second.link.msgForLink?.attaches is JsonArray
&& message.second.link.msgForLink?.attaches!!.jsonArray.isNotEmpty()) {
println("link $message")
for (attach in message.second.link.msgForLink.attaches!!.jsonArray) {
if (attach.jsonObject["_type"]?.jsonPrimitive?.content == "PHOTO") {
println(attach.jsonObject["baseUrl"]!!.jsonPrimitive.content)
images.add(attach.jsonObject["baseUrl"]!!.jsonPrimitive.content to true)
// lol its works
val photoToken = attach.jsonObject["photoToken"]!!.jsonPrimitive.content
// i think its cringe code :(
if (pickedPhoto == photoToken && !scrolled) {
coroutineScope.launch {
for (img in 0..images.size) {
if (images[img].first == attach.jsonObject["baseUrl"]!!.jsonPrimitive.content && images[img].second) {
pagerState.scrollToPage(img)
scrolled = true
break
}
}
}
}
}
}
}
}
}
AppTheme() {
Column (modifier = Modifier
.fillMaxSize()
.background(Color.Black)
.clickable(indication = null, interactionSource = interactionSource) {
isTopBar = !isTopBar
}
) {
if (isSingleImage) {
AsyncImage(
url,
"",
modifier = Modifier
.fillMaxSize()
.align(Alignment.CenterHorizontally),
contentScale = ContentScale.Fit,
alignment = Alignment.Center
)
} else {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxWidth(),
) { page ->
AsyncImage(
images[page].first,
"",
modifier = Modifier
.fillMaxSize()
.align(Alignment.CenterHorizontally),
contentScale = ContentScale.Fit,
alignment = Alignment.Center
)
}
}
}
AnimatedVisibility(visible = isTopBar, enter = fadeIn(), exit = fadeOut()) {
Box(modifier = Modifier.background(Color.Black.copy(
0.6f
))) {
Column(modifier = Modifier. padding(top = 30.dp, start = 8.dp, end = 8.dp)) {
Row() {
IconButton({
finish()
}) {
Icon(
Icons.AutoMirrored.Filled.ArrowBackIos,
"",
modifier = Modifier.size(25.dp),
tint = colorScheme.primary
)
}
Spacer(modifier = Modifier.weight(1f))
IconButton({
expanded = true
}) {
Icon(
Icons.Filled.MoreVert,
"",
modifier = Modifier.size(25.dp),
tint = colorScheme.primary
)
DropdownMenu(
expanded,
onDismissRequest = { expanded = false },
modifier = Modifier.background(colorScheme.secondaryContainer)
) {
DropdownMenuItem(
text = { Text(text = "Сохранить изображение", color = colorScheme.onSecondaryContainer)},
onClick = {
val filename = "IMG_${System.currentTimeMillis()}.png"
}
)
}
}
}
if (!isSingleImage) {
Text("${pagerState.currentPage + 1} из ${images.size}",
modifier = Modifier.align(Alignment.CenterHorizontally),
color = colorScheme.onSecondaryContainer
)
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,256 @@
package com.sffteam.voidclient
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.drawable.toBitmap
import androidx.datastore.preferences.core.stringPreferencesKey
import com.sffteam.voidclient.ui.theme.AppTheme
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonPrimitive
class MainActivity : ComponentActivity() {
@OptIn(
DelicateCoroutinesApi::class,
ExperimentalMaterial3WindowSizeClassApi::class,
ExperimentalMaterial3Api::class
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// Must be runBlocking because we need to wait for token check
runBlocking {
val exampleData = dataStore.data.first()
AccountManager.token = exampleData[stringPreferencesKey("token")].toString()
}
val context = this
val codes = mapOf(
"Россия" to "+7", "Беларусь" to "+375"
)
GlobalScope.launch {
withContext(Dispatchers.IO) {
SocketManager.connect(context)
}
}
if (AccountManager.token != "null") {
val intent = Intent(this, ChatListActivity::class.java)
this.startActivity(intent)
finish()
}
setContent {
AppTheme {
val phone = remember { mutableStateOf("") }
val errorText = remember { mutableStateOf("") }
var selectedCodeStr = remember { mutableStateOf("Россия") }
var selectedCode = remember { mutableStateOf("+7") }
Utils.windowSize = calculateWindowSizeClass(this)
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(colorScheme.background),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
val packageManager = context.packageManager
val appIconDrawable: Drawable =
packageManager.getApplicationIcon("com.sffteam.voidclient")
var expanded by remember { mutableStateOf(false) }
Image(
appIconDrawable.toBitmap(config = Bitmap.Config.ARGB_8888).asImageBitmap(),
contentDescription = "Image",
modifier = Modifier
.size(120.dp)
.padding(8.dp)
.clip(RoundedCornerShape(2.dp))
)
Text(
"Добро пожаловать в Void Client!",
fontSize = 25.sp,
textAlign = TextAlign.Center,
color = colorScheme.primary,
modifier = Modifier.padding(bottom = 2.dp)
)
Text(
"Введите свой номер телефона, чтобы войти или зарегистрироваться",
fontSize = 18.sp,
textAlign = TextAlign.Center,
color = colorScheme.primary,
modifier = Modifier.padding(bottom = 10.dp)
)
if (errorText.value.isNotEmpty()) {
Text(
"Ошибка: ${errorText.value}",
fontSize = 18.sp,
color = colorScheme.primary,
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.heightIn(60.dp)
) {
// TODO: Rewrite
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = it },
modifier = Modifier.width(180.dp)
) {
OutlinedTextField(
value = selectedCodeStr.value + " (${selectedCode.value})",
onValueChange = {},
readOnly = true,
maxLines = 1,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
modifier = Modifier
.menuAnchor()
.heightIn(max = 60.dp)
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
codes.toList().forEachIndexed { index, option ->
DropdownMenuItem(
text = {
Text(
option.first, modifier = Modifier.fillMaxSize()
)
},
onClick = {
selectedCodeStr.value = option.first
selectedCode.value = option.second
expanded = false
},
)
}
}
}
OutlinedTextField(
value = phone.value,
onValueChange = { newText ->
phone.value = newText
},
label = { Text("Номер телефона") },
textStyle = TextStyle(fontSize = 25.sp),
modifier = Modifier
.width(200.dp)
.height(60.dp),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
)
)
}
val context = LocalContext.current
Button(
modifier = Modifier.padding(16.dp), onClick = {
val packet = SocketManager.packPacket(
OPCode.START_AUTH.opcode, JsonObject(
mapOf(
"phone" to JsonPrimitive(selectedCode.value + phone.value),
"type" to JsonPrimitive("START_AUTH"),
"language" to JsonPrimitive("ru")
)
)
)
GlobalScope.launch {
SocketManager.sendPacket(
packet, { packet ->
println(packet.payload)
if (packet.payload is JsonObject) {
if ("error" in packet.payload) {
errorText.value =
packet.payload["localizedMessage"]?.jsonPrimitive?.content!!
} else if ("token" in packet.payload) {
val intent =
Intent(context, CodeActivity::class.java)
println("token " + packet.payload["token"])
intent.putExtra(
"token",
packet.payload["token"]!!.jsonPrimitive.content
)
context.startActivity(intent)
} else {
println("wtf")
}
}
})
}
}) {
Text("Продолжить", fontSize = 25.sp)
}
}
}
}
}
}

View File

@@ -0,0 +1,116 @@
package com.sffteam.voidclient
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.sp
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.sffteam.voidclient.ui.theme.AppTheme
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
class PasswordCheckActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_password_check2)
val trackId = intent.getStringExtra("trackId")
setContent {
val password = remember { mutableStateOf("") }
val errorText = remember { mutableStateOf("") }
val coroutineScope = rememberCoroutineScope()
AppTheme {
val context = LocalContext.current
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = password.value,
onValueChange = { newText -> password.value = newText },
label = { Text("Введите облачный пароль") },
textStyle = TextStyle(fontSize = 25.sp),
)
Text(
errorText.value, color = Color.White, fontSize = 25.sp
)
Button(onClick = {
val packet = SocketManager.packPacket(
OPCode.PASSWORD_CHECK.opcode, JsonObject(
mapOf(
"password" to JsonPrimitive(password.value),
"trackId" to JsonPrimitive(trackId)
)
)
)
coroutineScope.launch {
SocketManager.sendPacket(packet, { packet ->
if (packet.payload is JsonObject) {
if (packet.payload.containsKey("error")) {
errorText.value =
packet.payload["message"]!!.jsonPrimitive.content
} else {
val intent = Intent(
context, ChatListActivity::class.java
)
runBlocking {
dataStore.edit { settings ->
// Nice sandwich lol
val token =
packet.payload["tokenAttrs"]!!.jsonObject["LOGIN"]!!.jsonObject["token"]!!.jsonPrimitive.content
settings[stringPreferencesKey("token")] = token
AccountManager.token = token
}
}
GlobalScope.launch {
SocketManager.loginToAccount(context)
}
context.startActivity(intent)
finish()
}
}
})
}
}) {
Text("Войти")
}
}
}
}
}
}

View File

@@ -0,0 +1,319 @@
package com.sffteam.voidclient
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
import androidx.compose.material.icons.filled.Block
import androidx.compose.material.icons.filled.ChatBubble
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Text
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.sffteam.voidclient.ui.theme.AppTheme
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import java.util.Locale.getDefault
class ProfileViewActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val userId = intent.getLongExtra("userId", 0L)
setContent {
AppTheme() {
val users by UserManager.usersList.collectAsState()
val currentUser = users[userId]
val avatarUrl = currentUser?.avatarUrl
val description = currentUser?.description
val coroutineScope = rememberCoroutineScope()
val fullName = currentUser?.firstName + if (currentUser?.lastName?.isNotEmpty() == true) {
" " + currentUser.lastName
} else {
""
}
val context = LocalContext.current
var userMap : MutableMap<String, String> = mutableMapOf(
)
if (description?.isNotEmpty() == true) {
userMap["О себе"] = description
}
userMap["ID"] = userId.toString()
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(colorScheme.background)
.windowInsetsPadding(WindowInsets.statusBars)
) {
item {
IconButton({
finish()
}) {
Icon(
Icons.AutoMirrored.Filled.ArrowBackIos,
"",
modifier = Modifier.size(25.dp),
tint = colorScheme.primary
)
}
}
item {
Column(modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically)
) {
if (avatarUrl?.isNotEmpty() == true) {
AsyncImage(
avatarUrl,
"",
modifier = Modifier
.align(Alignment.CenterHorizontally)
.size(110.dp)
.clip(CircleShape)
.clickable {
val intent = Intent(context, ImageViewerActivity::class.java)
intent.putExtra("isSingleImage", true)
intent.putExtra("image", avatarUrl)
context.startActivity(intent)
},
contentScale = ContentScale.FillBounds
)
} else {
val initial =
fullName?.split(" ")?.mapNotNull { it.firstOrNull() }
?.take(2)?.joinToString("")?.uppercase(getDefault())
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.width(100.dp)
.height(100.dp)
.clip(CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(
Utils.getColorForAvatar(fullName).first,
Utils.getColorForAvatar(fullName).second
)
)
)
.align(Alignment.CenterHorizontally),
) {
Text(
text = initial.toString(),
color = Color.White,
style = MaterialTheme.typography.labelLarge,
fontSize = 40.sp
)
}
}
Text(
fullName,
fontSize = 24.sp,
modifier = Modifier.align(Alignment.CenterHorizontally).padding(start = 8.dp, end = 8.dp),
color = colorScheme.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
item {
Row(modifier = Modifier
.fillMaxWidth()
.padding(4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
) {
Button(onClick = {
val chatId = AccountManager.accountID xor userId
val packet = SocketManager.packPacket(
OPCode.CHAT_INFO.opcode, JsonObject(
mapOf(
"chatIds" to JsonArray(listOf(Json.encodeToJsonElement(Long.serializer(), chatId))),
)
)
)
val intent = Intent(context, ChatActivity::class.java)
try {
GlobalScope.launch {
SocketManager.sendPacket(
packet, { packet ->
println(packet)
GlobalScope.launch {
if (packet.payload is JsonObject) ChatManager.processChats(
packet.payload["chats"]!!.jsonArray
)
}
}
)
}
} catch (e : Exception) {
println(e)
}
intent.putExtra("chatID", chatId)
intent.putExtra("chatTitle", fullName);
intent.putExtra("chatType", "DIALOG")
intent.putExtra("chatIcon", avatarUrl);
context.startActivity(intent)
},
modifier = Modifier.padding(4.dp).width(160.dp),
colors = ButtonColors(
containerColor = colorScheme.primaryContainer,
contentColor = colorScheme.onPrimaryContainer,
disabledContainerColor = colorScheme.primaryContainer,
disabledContentColor = colorScheme.onPrimaryContainer,
)
) {
Column {
Icon(
Icons.Filled.ChatBubble,
"",
modifier = Modifier.size(25.dp).align(Alignment.CenterHorizontally)
)
Text("Чат",
fontSize = 18.sp,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
}
}
Button(onClick = {
},
modifier = Modifier.padding(4.dp).width(160.dp),
colors = ButtonColors(
containerColor = colorScheme.primaryContainer,
contentColor = colorScheme.onPrimaryContainer,
disabledContainerColor = colorScheme.primaryContainer,
disabledContentColor = colorScheme.onPrimaryContainer,
)
) {
Column() {
Icon(
Icons.Filled.Block,
"",
modifier = Modifier.size(25.dp).align(Alignment.CenterHorizontally)
)
Text("Заблокировать",
fontSize = 18.sp,
modifier = Modifier.align(Alignment.CenterHorizontally),
maxLines = 1
)
}
}
//
// For future updates
// Button(onClick = {
// },
// modifier = Modifier.padding(4.dp).widthIn(min = 100.dp),
// colors = ButtonColors(
// containerColor = colorScheme.primaryContainer,
// contentColor = colorScheme.onPrimaryContainer,
// disabledContainerColor = colorScheme.primaryContainer,
// disabledContentColor = colorScheme.onPrimaryContainer,
// )
// ) {
// Column() {
// Icon(
// Icons.Filled.Folder,
// "",
// modifier = Modifier.size(25.dp).align(Alignment.CenterHorizontally)
// )
// Text("Добавить в папку",
// fontSize = 18.sp,
// modifier = Modifier.align(Alignment.CenterHorizontally),
// maxLines = 1
// )
// }
// }
}
}
item {
Box(modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.background(colorScheme.secondaryContainer, RoundedCornerShape(16.dp))
) {
Column(modifier = Modifier.padding(8.dp)) {
for (setting in userMap.toList()) {
Text(setting.first, modifier = Modifier.alpha(0.7f),
color = colorScheme.onSecondaryContainer, fontSize = 16.sp)
Text(setting.second,
color = colorScheme.onSecondaryContainer,
fontSize = 20.sp,
modifier = Modifier.padding(bottom = 4.dp)
)
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,134 @@
package com.sffteam.voidclient
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.sffteam.voidclient.ui.theme.AppTheme
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonPrimitive
class RegisterActivity : ComponentActivity() {
@OptIn(DelicateCoroutinesApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val token = intent.getStringExtra("token")
setContent {
AppTheme {
val firstName = remember { mutableStateOf("") }
val lastName = remember { mutableStateOf("") }
val errorText = remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = firstName.value,
onValueChange = { newText -> firstName.value = newText },
label = { Text("Имя") },
textStyle = TextStyle(fontSize = 25.sp),
modifier = Modifier.padding(bottom = 15.dp)
)
OutlinedTextField(
value = lastName.value,
onValueChange = { newText -> lastName.value = newText },
label = { Text("Фамилия (необязательно)") },
textStyle = TextStyle(fontSize = 25.sp),
)
Text(
errorText.value, color = Color.White, fontSize = 25.sp
)
val context = LocalContext.current
Button(onClick = {
println("fff${firstName.value}fff")
if (firstName.value.isEmpty()) {
errorText.value = "Имя не может быть пустым!"
} else {
val payload = mutableMapOf(
"firstName" to JsonPrimitive(firstName.value),
)
if (lastName.value.isNotEmpty()) {
payload["lastName"] = JsonPrimitive(lastName.value)
}
payload["tokenType"] = JsonPrimitive("REGISTER")
payload["token"] = JsonPrimitive(token)
val packet = SocketManager.packPacket(23, JsonObject(payload))
GlobalScope.launch {
SocketManager.sendPacket(packet, { packet ->
if (packet.payload is JsonObject) {
if ("error" in packet.payload) {
errorText.value =
packet.payload["localizedMessage"]!!.jsonPrimitive.content
} else if ("token" in packet.payload) {
val intent =
Intent(context, ChatListActivity::class.java)
runBlocking {
dataStore.edit { settings ->
// Nice sandwich lol
val tokenSettings =
packet.payload["token"]!!.jsonPrimitive.content
settings[stringPreferencesKey("token")] =
tokenSettings
AccountManager.token = tokenSettings
}
}
GlobalScope.launch {
SocketManager.loginToAccount(context)
}
context.startActivity(intent)
finish()
} else {
println("wtf")
}
}
})
}
}
}) {
Text("Продолжить", fontSize = 25.sp)
}
}
}
}
}
}

View File

@@ -0,0 +1,490 @@
package com.sffteam.voidclient
import android.content.Context
import android.content.Intent
import android.icu.util.TimeZone
import android.os.Build
import android.provider.Settings
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.fasterxml.jackson.databind.ObjectMapper
import io.ktor.network.selector.SelectorManager
import io.ktor.network.sockets.Socket
import io.ktor.network.sockets.aSocket
import io.ktor.network.sockets.openReadChannel
import io.ktor.network.sockets.openWriteChannel
import io.ktor.network.tls.tls
import io.ktor.utils.io.cancel
import io.ktor.utils.io.readAvailable
import io.ktor.utils.io.writeFully
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import net.jpountz.lz4.LZ4Factory
import net.jpountz.lz4.LZ4FastDecompressor
import org.msgpack.jackson.dataformat.MessagePackFactory
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.Locale
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.time.Duration.Companion.seconds
const val host = "api.oneme.ru"
const val port = 443
const val API_VERSION = 10 // lol
var Seq = 1
enum class OPCode(val opcode: Int) {
PING(1),
START(6), // Using that on open socket
CHANGE_PROFILE(16),
START_AUTH(17),
CHECK_CODE(18), // Also can be LOGIN packet from server or WRONG_CODE from server
PROFILE_INFO(19), // Server returns profile info with that opcode
LOGOUT(20),
SETTINGS_CHANGE(22),
NEW_STICKER_SETS(26), // Idk, implement it later
SYNC_EMOJI(27), // Also syncs ANIMOJI, REACTIONS, STICKERS, FAVORITE_STICKER
ANIMOJI(28), // Idk
CONTACTS_INFO(32), // Returns info about ids that your sent (if you sent ids that not your contacts, server return you just a empty array)
LAST_SEEN(35), // Used for obtain last seen of contacts
CHAT_INFO(48),
CHAT_MESSAGES(49),
EDIT_CHAT_INFO(55),
JOIN_CHAT(57),
LEAVE_CHAT(58),
SEND_MESSAGE(64),
DELETE_MESSAGE(66),
EDIT_MESSAGE(67),
CHAT_SUBSCRIBE(75), // Idk
WHO_CAN_SEE(76), // Used for disable or enable status online
EDIT_ADMIN_PERMISSION(77),
HISTORY(79), // Idk
UPLOAD_IMAGE(80),
GET_FILE(88),
SESSIONS(96), // Used for obtain all sessions for account
SETTINGS_UPDATE(134),
SESSIONS_EXIT(97),
PASSWORD_CHECK(115),
SYNC_FOLDER(272),
QR_CODE(290)
}
@Serializable
data class Packet(
@SerialName("ver") val ver: Int = API_VERSION,
@SerialName("cmd") val cmd: Int = 0,
@SerialName("seq") val seq: Int = Seq,
@SerialName("opcode") val opcode: Int,
@SerialName("payload") @Contextual val payload: JsonElement,
)
data class PacketCallback(val seq: Int, val callback: (Packet) -> Unit)
fun Short.toByteArrayBigEndian(): ByteArray {
return ByteBuffer.allocate(Short.SIZE_BYTES).putShort(this).array()
}
fun Int.toByteArrayBigEndian(): ByteArray {
return byteArrayOf(
(this ushr 24).toByte(), (this ushr 16).toByte(), (this ushr 8).toByte(), this.toByte()
)
}
fun messagePackToJson(bytes: ByteArray): String {
val msgpackMapper = ObjectMapper(MessagePackFactory())
val jsonMapper = ObjectMapper()
val node = msgpackMapper.readTree(bytes)
return jsonMapper.writeValueAsString(node)
}
fun jsonToMessagePack(json: String): ByteArray {
val jsonMapper = ObjectMapper()
val msgPackMapper = ObjectMapper(MessagePackFactory())
val tree = jsonMapper.readTree(json)
return msgPackMapper.writeValueAsBytes(tree)
}
object SocketManager {
private val selectorManager = SelectorManager(Dispatchers.IO)
private lateinit var socket: Socket
private val subscribers = CopyOnWriteArrayList<(String) -> Unit>()
private var packetCallbacks = mutableListOf<PacketCallback>()
fun packPacket(opcode: Int, payload: JsonElement): ByteArray {
// Thanks to https://github.com/ink-developer/PyMax/blob/main/src/pymax/mixins/socket.py#L75 again :D
val apiVer = API_VERSION.toByte()
val cmd = 0.toByte()
val seq = Seq.toShort().toByteArrayBigEndian()
val opcode = opcode.toShort().toByteArrayBigEndian()
println("string ${payload.toString()}")
val payload = jsonToMessagePack(payload.toString())
val payloadLen = payload.size and 0xFFFFFF
return byteArrayOf(
apiVer, cmd, *seq, *opcode, *payloadLen.toByteArrayBigEndian(), *payload
)
}
fun unpackPacket(data: ByteArray): Packet {
// Thanks to https://github.com/ink-developer/PyMax/blob/main/src/pymax/mixins/socket.py#L42
val factory = LZ4Factory.fastestInstance()
val decompressor: LZ4FastDecompressor = factory.fastDecompressor()
val apiVer = data[0].toInt() and 0xFF
val cmd = data[1].toInt() and 0xFF
val seqSigned = ByteBuffer.wrap(data, 2, 2).order(ByteOrder.BIG_ENDIAN).short
val seq = seqSigned.toInt() and 0xFFFF
val opcodeSigned = ByteBuffer.wrap(data, 4, 2).order(ByteOrder.BIG_ENDIAN).short
val opcode = opcodeSigned.toInt() and 0xFFFF
val packedLen =
ByteBuffer.wrap(data, 6, 4).order(ByteOrder.BIG_ENDIAN).int.toLong() and 0xFFFFFFFFL
val compFlag = (packedLen shr 24).toInt()
val payloadLength = (packedLen and 0xFFFFFF).toInt()
val payloadBytes = data.sliceArray(10 until (10 + payloadLength))
var payload = ""
if (payloadBytes.isNotEmpty()) {
if (compFlag != 0) {
var decompressedBytes = ByteArray(131072)
println("test1")
try {
decompressor.decompress(payloadBytes, decompressedBytes)
} catch (e: Exception) {
println("decomp err ${e}")
}
println("test2")
try {
payload = messagePackToJson(decompressedBytes)
} catch (e: Exception) {
println(e)
}
} else {
payload = messagePackToJson(payloadBytes)
}
}
println("payload! ${payload}")
var jsonPayload = JsonObject(emptyMap())
if (payload.isNotEmpty()) {
jsonPayload = Json.decodeFromString(payload)
}
return Packet(
apiVer, cmd, seq, opcode, jsonPayload
)
}
suspend fun sendStartPacket(context: Context): Boolean {
sendPacket(
packPacket(
OPCode.START.opcode, JsonObject(
mapOf(
"clientSessionId" to JsonPrimitive(192L), "userAgent" to JsonObject(
mapOf(
"deviceType" to JsonPrimitive("ANDROID"),
"appVersion" to JsonPrimitive("25.21.0"),
"osVersion" to JsonPrimitive("Android ${Build.VERSION.RELEASE}"),
"timezone" to JsonPrimitive(TimeZone.getDefault().id),
"screen" to JsonPrimitive("382dpi 382dpi 1080x2243"),
"pushDeviceType" to JsonPrimitive("GCM"),
"locale" to JsonPrimitive("ru"),
"buildNumber" to JsonPrimitive(6420),
"deviceName" to JsonPrimitive(Build.MANUFACTURER + " " + Build.MODEL),
"deviceLocale" to JsonPrimitive(Locale.getDefault().language.toString()),
)
), "deviceId" to JsonPrimitive(
Settings.Secure.getString(
context.contentResolver, Settings.Secure.ANDROID_ID
)
)
)
)
), { packet ->
println("response")
println(packet.payload)
})
return true
}
suspend fun connect(context: Context) = coroutineScope {
println("trying to connect")
while (true) {
try {
socket = aSocket(selectorManager).tcp().connect(host, port)
.tls(coroutineContext = currentCoroutineContext())
val result = sendStartPacket(context)
if (result) {
if (AccountManager.token != "null") {
loginToAccount(context)
AccountManager.logined = true
}
async {
sendPing()
}
getPackets()
}
} catch (e: Exception) {
println(e)
}
delay(50)
}
}
suspend fun loginToAccount(context: Context) = coroutineScope {
val packet = packPacket(
OPCode.PROFILE_INFO.opcode, JsonObject(
mapOf(
"interactive" to JsonPrimitive(true),
"token" to JsonPrimitive(AccountManager.token),
"chatsCount" to JsonPrimitive(40),
"chatsSync" to JsonPrimitive(0),
"contactsSync" to JsonPrimitive(0),
"presenceSync" to JsonPrimitive(0),
"draftsSync" to JsonPrimitive(0),
)
)
)
sendPacket(
packet, { packet ->
if (packet.payload.jsonObject.containsKey("error")) {
val intent: Intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
AccountManager.token = "null"
println(AccountManager.token)
runBlocking {
try {
context.dataStore.edit { settings ->
settings[stringPreferencesKey("token")] = "null"
}
} catch (e: Exception) {
println(e)
}
}
context.startActivity(intent)
} else {
println(packet)
try {
AccountManager.accountID =
packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["id"]!!.jsonPrimitive.long
AccountManager.phone =
packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["phone"]!!.jsonPrimitive.content
UserManager.processMyProfile(packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject)
AccountManager.processSettings(packet.payload.jsonObject["config"]!!.jsonObject["user"]!!.jsonObject)
val packet = SocketManager.packPacket(
OPCode.CONTACTS_INFO.opcode, JsonObject(
mapOf(
"contactIds" to JsonArray(listOf(JsonPrimitive(AccountManager.accountID))),
)
)
)
GlobalScope.launch {
sendPacket(
packet, { packet ->
println(packet.payload)
if (packet.payload is JsonObject) {
GlobalScope.launch {
UserManager.processUsers(packet.payload["contacts"]!!.jsonArray)
}
}
}
)
}
} catch (e: Exception) {
println(e)
}
try {
val test = packet.payload.jsonObject["chats"]!!.jsonArray
println(test)
} catch (e: Exception) {
println(e)
}
println()
GlobalScope.launch {
ChatManager.processChats(packet.payload.jsonObject["chats"]!!.jsonArray)
}
}
})
}
suspend fun sendPacket(packet: ByteArray, callback: (Packet) -> Unit) {
val sendChannel = socket.openWriteChannel(autoFlush = true)
println(unpackPacket(packet))
sendChannel.writeFully(packet)
sendChannel.flush()
packetCallbacks.add(PacketCallback(Seq, callback))
Seq += 1
}
suspend fun getPackets() {
val receiveChannel = socket.openReadChannel()
try {
var entirePacket = ByteArray(131072)
var pos = 0
while (socket.isActive) {
val buffer = ByteArray(8192)
val bytesRead = receiveChannel.readAvailable(buffer, 0, 8192)
if (bytesRead == -1) {
break
}
println(bytesRead)
println(buffer.size)
if (bytesRead > 0) {
if (bytesRead == 8192) { // tmp solution
entirePacket = buffer.copyInto(entirePacket, pos)
pos += 8192
continue
}
entirePacket = buffer.copyInto(entirePacket, pos, 0, bytesRead)
pos += bytesRead
println("Total packet length: ${pos}")
val packet = unpackPacket(entirePacket.sliceArray(0..<pos))
pos = 0
if (packet.opcode == 135) {
if (packet.payload.jsonObject["chat"]?.jsonObject["status"]?.jsonPrimitive?.content == "REMOVED") {
ChatManager.removeChat(packet.payload.jsonObject["chat"]?.jsonObject["id"]?.jsonPrimitive?.long!!)
} else {
ChatManager.processSingleChat(packet.payload.jsonObject["chat"]!!.jsonObject)
}
}
if (packet.opcode == 128) {
var textForwarded: String = ""
var senderForwarded: Long = 0L
var msgForwardedID: String = ""
var forwardedAttaches: JsonElement? = JsonNull
var forwardedType: String = ""
if (packet.payload.jsonObject.contains("chat")) {
GlobalScope.launch {
ChatManager.processSingleChat(packet.payload.jsonObject["chat"]!!.jsonObject)
}
}
if (packet.payload.jsonObject["message"]!!.jsonObject.contains("link")) {
val messageLinked =
packet.payload.jsonObject["message"]?.jsonObject["link"]?.jsonObject["message"]
textForwarded =
messageLinked?.jsonObject["text"]?.jsonPrimitive?.content.toString()
senderForwarded =
messageLinked?.jsonObject["sender"]?.jsonPrimitive!!.long
msgForwardedID =
messageLinked.jsonObject["id"]?.jsonPrimitive!!.long.toString()
forwardedType =
packet.payload.jsonObject["message"]?.jsonObject["link"]?.jsonObject["type"]?.jsonPrimitive?.content.toString()
}
ChatManager.addMessage(
packet.payload.jsonObject["message"]?.jsonObject["id"]!!.jsonPrimitive.content,
Message(
packet.payload.jsonObject["message"]?.jsonObject["text"]!!.jsonPrimitive.content,
packet.payload.jsonObject["message"]?.jsonObject["time"]!!.jsonPrimitive.long,
packet.payload.jsonObject["message"]?.jsonObject["sender"]!!.jsonPrimitive.long,
if (packet.payload.jsonObject["message"]?.jsonObject?.contains("attaches") == true) packet.payload.jsonObject["message"]?.jsonObject["attaches"]!!.jsonArray else JsonArray(
emptyList()
),
if (packet.payload.jsonObject["message"]?.jsonObject?.contains("status") == true) packet.payload.jsonObject["message"]?.jsonObject["status"]!!.jsonPrimitive.content else "",
MessageLink(
type = forwardedType, msgForLink = msgForLink(
message = textForwarded,
senderID = senderForwarded,
attaches = forwardedAttaches,
msgID = msgForwardedID
)
)
),
packet.payload.jsonObject["chatId"]?.jsonPrimitive?.long ?: 0L
)
}
if (packet.opcode == OPCode.SETTINGS_UPDATE.opcode) {
AccountManager.processSettings(packet.payload.jsonObject["config"]!!.jsonObject["user"]!!.jsonObject)
}
run loop@{
SocketManager.packetCallbacks.forEachIndexed { i, cb ->
if (cb.seq == packet.seq) {
cb.callback(packet)
SocketManager.packetCallbacks.removeAt(i)
return@loop
}
}
}
println(packet)
println()
}
}
} catch (e: Exception) {
println(e)
} finally {
receiveChannel.cancel()
socket.close()
}
}
suspend fun sendPing() {
val packet = packPacket(
OPCode.PING.opcode, JsonObject(
mapOf(
"interactive" to JsonPrimitive(false),
)
)
)
while (true) {
delay(20.seconds)
sendPacket(packet, {})
println("ping!")
}
}
}

View File

@@ -0,0 +1,204 @@
package com.sffteam.voidclient
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
data class User(
val avatarUrl: String, val firstName: String, val lastName: String, val lastSeen: Long, val description : String
)
object UserManager {
private val _usersList = MutableStateFlow<Map<Long, User>>(emptyMap())
var usersList = _usersList.asStateFlow()
fun clearUsersList() {
_usersList.update {
emptyMap()
}
}
fun processMyProfile(profile : JsonObject) {
println("myprofile $profile")
var userID = 0L
try {
userID = profile.jsonObject["id"]!!.jsonPrimitive.long
} catch (e: Exception) {
println(e)
}
println(userID)
try {
var avatarUrl = ""
var firstName = ""
var lastName = ""
var lastSeen = 0L
var desc = ""
try {
avatarUrl = profile.jsonObject["baseUrl"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
println("0msg")
}
try {
firstName =
profile.jsonObject["names"]!!.jsonArray[0].jsonObject["firstName"]?.jsonPrimitive!!.content
} catch (e: Exception) {
println("1msg")
println(e)
}
try {
lastName =
profile.jsonObject["names"]!!.jsonArray[0].jsonObject["lastName"]?.jsonPrimitive!!.content
} catch (e: Exception) {
println("5msg")
println(e)
}
val currentMap = mapOf(
userID to User(
avatarUrl, firstName, lastName, 0L, desc
)
)
_usersList.update {
it.toMap() + currentMap
}
} catch (e: Exception) {
println(e)
}
}
// TODO: Process users presence
fun processPresence(presences: JsonArray) {
for (i in presences.jsonObject.toList()) {
val prs = i.second.jsonObject["seen"]?.jsonPrimitive?.long
_usersList.update { oldMap ->
oldMap + (i.first.toLong() to User(
oldMap[i.first.toLong()]?.avatarUrl ?: "",
oldMap[i.first.toLong()]?.firstName ?: "",
oldMap[i.first.toLong()]?.lastName ?: "",
i.second.jsonObject["seen"]?.jsonPrimitive?.long ?: 0L,
oldMap[i.first.toLong()]?.description ?: ""
))
}
}
}
fun processUsers(contacts: JsonArray) {
println("cool users $contacts")
for (i in contacts) {
var userID: Long = 0
try {
userID = i.jsonObject["id"]!!.jsonPrimitive.long
} catch (e: Exception) {
println(e)
}
println(userID)
try {
var avatarUrl = ""
var firstName = ""
var lastName = ""
var lastSeen = 0L
var desc = ""
try {
avatarUrl = i.jsonObject["baseUrl"]!!.jsonPrimitive.content
} catch (e: Exception) {
println(e)
println("0msg")
}
try {
firstName =
i.jsonObject["names"]!!.jsonArray[0].jsonObject["firstName"]?.jsonPrimitive!!.content
} catch (e: Exception) {
println("1msg")
println(e)
}
try {
lastName =
i.jsonObject["names"]!!.jsonArray[0].jsonObject["lastName"]?.jsonPrimitive!!.content
} catch (e: Exception) {
println("5msg")
println(e)
}
try {
desc =
i.jsonObject["description"]!!.jsonPrimitive.content
} catch (e: Exception) {
println("5msg")
println(e)
}
val currentMap = mapOf(
userID to User(
avatarUrl, firstName, lastName, 0L, desc
)
)
_usersList.update {
it.toMap() + currentMap
}
} catch (e: Exception) {
println(e)
}
println(_usersList.value.toMap())
println("processing")
}
}
fun checkForExisting(user: Long) {
if (!usersList.value.containsKey(user)) {
val packet = SocketManager.packPacket(
OPCode.CONTACTS_INFO.opcode, JsonObject(
mapOf(
"contactIds" to JsonArray(
listOf(
Json.encodeToJsonElement(
Long.serializer(), user
)
)
),
)
)
)
GlobalScope.launch {
SocketManager.sendPacket(
packet, { packet ->
println(packet.payload)
if (packet.payload is JsonObject) {
GlobalScope.launch {
if (packet.payload["contacts"]?.jsonArray?.isNotEmpty() == true) {
processUsers(packet.payload["contacts"]!!.jsonArray)
}
}
}
}
)
}
}
}
}

View File

@@ -0,0 +1,91 @@
package com.sffteam.voidclient
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Article
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
import androidx.compose.material.icons.filled.Article
import androidx.compose.material.icons.filled.AudioFile
import androidx.compose.material.icons.filled.InsertDriveFile
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import java.io.File
import kotlin.math.absoluteValue
object Utils {
lateinit var windowSize: WindowSizeClass
val audioExtensions = listOf(
"mp3", "aac", "m4a", "ogg", "oga", "opus", "wma", "amr", "3gp",
"flac", "alac", "ape", "wav", "aiff", "aif", "aifc", "wv", "tta", "tak", "shn",
"dsf", "dff", "dsd", "pcm", "dxp", "pt24",
"mid", "midi", "rmi", "kar",
"mod", "xm", "s3m", "it", "mtm", "umx", "mo3",
"caf", "au", "snd", "ra", "rm", "mka", "weba", "ac3", "eac3", "dts", "m4b",
"voc", "8svx", "cda", "gsm", "mpc", "spx", "la"
)
fun getIconForFile(file : String) : ImageVector {
val extension = File(file).extension
if (extension == "txt") {
return Icons.AutoMirrored.Filled.Article
}
for (i in audioExtensions) {
if (extension == i) {
return Icons.Filled.AudioFile
}
}
return Icons.AutoMirrored.Filled.InsertDriveFile
}
fun getSizeFromBytes(bytes : Long) : String {
if (bytes <= 1000) {
return "$bytes B"
}
if (bytes in 1000..<1000000) {
val kb = bytes / 1000
return "$kb KB"
}
if (bytes > 1000000) {
val mb = bytes / 1000000
return "$mb MB"
}
return "$bytes"
}
fun getColorForAvatar(avatar: String): Pair<Color, Color> {
val colors = listOf(
Pair(Color(0xFFFF0026), Color(0xFFFF00BB)),
Pair(Color(0xFFFFC004), Color(0xFFFFE59F)),
Pair(Color(0xFF0A5BC2), Color(0xFF3B8FFF)),
Pair(Color(0xFF04C715), Color(0xFF6AFC78)),
Pair(Color(0xFFA308C4), Color(0xFFE071FC)),
)
val index = (avatar.hashCode().absoluteValue) % colors.size
return colors[index]
}
fun getColorForNickname(nickName: String): Color {
val colors = listOf(
Color(0xFFFF2B4B),
Color(0xFFF8C324),
Color(0xFFFD903C),
Color(0xFF2196F3),
Color(0xFF2BFF47),
Color(0xFFE139FF),
Color(0xFF41E0D2),
Color(0xFF7826FC),
)
val index = (nickName.hashCode().absoluteValue) % colors.size
return colors[index]
}
}

View File

@@ -0,0 +1,244 @@
package com.sffteam.voidclient.preferences
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.drawable.toBitmap
import com.sffteam.voidclient.R
import com.sffteam.voidclient.ui.theme.AppTheme
import sh.calvin.autolinktext.rememberAutoLinkText
class AboutActivity : ComponentActivity() {
val specialThanks = listOf(
"Kolyah35",
"CITRIM",
"DeL",
"FullHarmony",
"danilka22ah",
"njuyse",
"TeamKomet",
"a555lieva",
"Irishka_Piper",
)
val developers = mapOf(
"InviseDivine" to "Разработчик, дизайнер, основатель проекта",
"Jaan" to "Помощь с разработкой SocketManager'а"
)
val infoText = "Void Client - самописный клиент для MAX'а с открытым исходным кодом"
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_about)
setContent {
AppTheme() {
val context = LocalContext.current
Scaffold(
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = colorScheme.surfaceContainer,
),
title = {
Text("О приложении")
},
navigationIcon = {
IconButton({ finish() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBackIos,
contentDescription = "Меню"
)
}
},
)
}) {
LazyColumn(
modifier = Modifier
.padding(it)
.fillMaxWidth()
.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
item {
val packageManager = context.packageManager
val appIconDrawable: Drawable =
packageManager.getApplicationIcon("com.sffteam.voidclient")
Image(
appIconDrawable.toBitmap(config = Bitmap.Config.ARGB_8888)
.asImageBitmap(),
contentDescription = "Image", modifier = Modifier
.size(100.dp)
.padding(8.dp)
.clip(CircleShape)
)
}
item {
Box(
modifier = Modifier
.background(
colorScheme.secondaryContainer,
shape = RoundedCornerShape(20.dp)
)
.fillMaxWidth()
.padding(start = 4.dp, top = 4.dp, end = 4.dp)
) {
Column() {
Text(
"О приложении",
fontSize = 24.sp,
color = colorScheme.primary,
modifier = Modifier.padding(start = 4.dp, bottom = 8.dp)
)
Column() {
Text(
infoText,
fontSize = 22.sp,
modifier = Modifier.padding(start = 4.dp, bottom = 8.dp)
)
Text(
AnnotatedString.rememberAutoLinkText(
"Наши ссылки: \n" +
"Github - https://github.com/InviseDivine/Void-Client \n" +
"Telegram - t.me/max_voidclient",
),
fontSize = 20.sp,
modifier = Modifier.padding(start = 4.dp, bottom = 8.dp)
)
}
}
}
}
item {
Box(
modifier = Modifier
.background(
colorScheme.secondaryContainer,
shape = RoundedCornerShape(20.dp)
)
.fillMaxWidth()
.padding(start = 4.dp, top = 4.dp, end = 4.dp)
) {
Column() {
Text(
"Разработчики",
fontSize = 24.sp,
color = colorScheme.onSecondaryContainer,
modifier = Modifier.padding(start = 4.dp, bottom = 8.dp)
)
for (dev in developers.toList()) {
DrawDevelopers(dev.first, dev.second)
}
}
}
}
item {
Box(
modifier = Modifier
.background(
colorScheme.secondaryContainer,
shape = RoundedCornerShape(20.dp)
)
.fillMaxWidth()
.padding(start = 4.dp, top = 4.dp, end = 4.dp)
) {
Column() {
Text(
"Отдельная благодарность",
fontSize = 24.sp,
color = colorScheme.onSecondaryContainer,
modifier = Modifier.padding(start = 4.dp, bottom = 8.dp)
)
for (people in specialThanks) {
DrawSpecialThanks(people)
}
}
}
}
}
}
}
}
}
}
@Composable
fun DrawDevelopers(people: String, desc: String) {
// TODO : Avatars
Column() {
Text(
people,
fontSize = 22.sp,
modifier = Modifier
.padding(start = 4.dp, bottom = 2.dp),
color = colorScheme.onSecondaryContainer,
)
Text(
desc,
fontSize = 18.sp,
modifier = Modifier
.padding(start = 4.dp, bottom = 4.dp)
.alpha(0.7f),
color = colorScheme.onSecondaryContainer,
)
}
}
@Composable
fun DrawSpecialThanks(people: String) {
Text(
people,
fontSize = 20.sp,
modifier = Modifier
.padding(start = 4.dp, bottom = 4.dp)
.alpha(0.8f),
color = colorScheme.onSecondaryContainer,
)
}

View File

@@ -0,0 +1,92 @@
package com.sffteam.voidclient.preferences
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.sffteam.voidclient.AccountManager
import com.sffteam.voidclient.OPCode
import com.sffteam.voidclient.SocketManager
import com.sffteam.voidclient.ui.theme.AppTheme
import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
class ChatSettingsActivity : AppCompatActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val coroutineScope = rememberCoroutineScope()
if (AccountManager.sessionsList.value.isEmpty()) {
val packet =
SocketManager.packPacket(OPCode.SESSIONS.opcode, JsonObject(emptyMap()))
coroutineScope.launch {
SocketManager.sendPacket(packet, { packet ->
if (packet.payload is JsonObject) {
AccountManager.processSession(packet.payload["sessions"]!!.jsonArray)
}
})
}
}
AppTheme {
Scaffold(
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = colorScheme.surfaceContainer,
),
title = {
Text("Настройки чатов")
},
navigationIcon = {
IconButton({ finish() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBackIos,
contentDescription = "Меню"
)
}
},
)
}
) {
LazyColumn(
modifier = Modifier
.padding(it)
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp, bottom = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
item() {}
item() {}
item() {}
}
}
}
}
}
}

View File

@@ -0,0 +1,272 @@
package com.sffteam.voidclient.preferences
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
import androidx.compose.material.icons.outlined.Android
import androidx.compose.material.icons.outlined.PhoneIphone
import androidx.compose.material.icons.outlined.Web
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight.Companion.Bold
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.sffteam.voidclient.AccountManager
import com.sffteam.voidclient.OPCode
import com.sffteam.voidclient.Session
import com.sffteam.voidclient.SocketManager
import com.sffteam.voidclient.dataStore
import com.sffteam.voidclient.ui.theme.AppTheme
import io.github.g00fy2.quickie.QRResult
import io.github.g00fy2.quickie.ScanQRCode
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import java.time.Duration
import java.util.Date
import kotlin.time.ExperimentalTime
import kotlin.time.Instant.Companion.fromEpochMilliseconds
class DevicesActivity : ComponentActivity() {
val scanQrCodeLauncher = registerForActivityResult(ScanQRCode()) { result ->
if (result is QRResult.QRSuccess) {
val packet = SocketManager.packPacket(
OPCode.QR_CODE.opcode, JsonObject(
mapOf(
"qrLink" to JsonPrimitive(result.content.rawValue.toString())
)
)
)
runBlocking {
SocketManager.sendPacket(packet, { packet ->
if (packet.payload is JsonObject) {
println(packet.payload)
}
})
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val coroutineScope = rememberCoroutineScope()
if (AccountManager.sessionsList.collectAsState().value.isEmpty()) {
val packet =
SocketManager.packPacket(OPCode.SESSIONS.opcode, JsonObject(emptyMap()))
coroutineScope.launch {
SocketManager.sendPacket(packet, { packet ->
if (packet.payload is JsonObject) {
AccountManager.processSession(packet.payload["sessions"]!!.jsonArray)
}
})
}
}
AppTheme {
val context = LocalContext.current
Scaffold(
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = colorScheme.surfaceContainer,
),
title = {
Text("Устройства")
},
navigationIcon = {
IconButton({ finish() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBackIos,
contentDescription = "Меню"
)
}
},
)
}) {
LazyColumn(
modifier = Modifier
.padding(it)
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp, bottom = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
item() {
Button(onClick = {
scanQrCodeLauncher.launch(null)
}) {
Text("Войти по QR коду", fontSize = 18.sp)
}
}
item() {
Button(onClick = {
val packet = SocketManager.packPacket(
OPCode.SESSIONS_EXIT.opcode,
JsonObject(emptyMap())
)
coroutineScope.launch {
SocketManager.sendPacket(packet, { packet ->
if (packet.payload is JsonObject) {
println(packet.payload)
try {
runBlocking {
context.dataStore.edit { settings ->
settings[stringPreferencesKey("token")] =
packet.payload["token"]!!.jsonPrimitive.content
SocketManager.loginToAccount(context)
}
}
} catch (e: Exception) {
println(e)
}
}
})
}
}) {
Text("Завершить все сеансы", fontSize = 18.sp, color = Color.Red)
}
}
item() {
Box(
modifier = Modifier
.background(
colorScheme.secondaryContainer,
shape = RoundedCornerShape(20.dp)
)
.fillMaxWidth()
.padding(start = 4.dp, top = 4.dp, end = 4.dp)
) {
Column() {
val sessions by AccountManager.sessionsList.collectAsState()
Column(modifier = Modifier) {
Text(
"Активные сеансы",
fontSize = 22.sp,
color = colorScheme.onSecondaryContainer,
modifier = Modifier.padding(start = 4.dp, bottom = 8.dp)
)
for (session in sessions.sortedByDescending { value -> value.time }) {
DrawSessions(session)
}
}
}
}
}
}
}
}
}
}
}
@OptIn(ExperimentalTime::class)
@Composable
fun DrawSessions(session: Session) {
Box(modifier = Modifier.padding(bottom = 8.dp)) {
Row() {
val lastMessageTime = session.time
val currentTime = Date().time
val instantLast = fromEpochMilliseconds(lastMessageTime)
val duration = Duration.ofSeconds(currentTime / 1000 - lastMessageTime / 1000)
val localDateTime = instantLast.toLocalDateTime(TimeZone.currentSystemDefault())
val hours = if (localDateTime.hour < 10) {
"0${localDateTime.hour}"
} else {
localDateTime.hour
}
val minutes = if (localDateTime.minute < 10) {
"0${localDateTime.minute}"
} else {
localDateTime.minute
}
val time = if (duration.toHours() < 24) {
"${hours}:${minutes}"
} else {
"${hours}:${minutes} ${localDateTime.date}"
}
val icon = if (session.client == "MAX WEB") {
Icons.Outlined.Web
} else if (session.client == "MAX Android") {
Icons.Outlined.Android
} else {
// TODO : Change to IOS icon
Icons.Outlined.PhoneIphone
}
Icon(
icon,
"lol",
modifier = Modifier
.size(40.dp)
.align(Alignment.CenterVertically)
.padding(end = 4.dp), tint = colorScheme.onSecondaryContainer
)
Column(modifier = Modifier.weight(0.8f)) {
val client = if (session.current) {
session.client + " (Текущая)"
} else {
session.client
}
Text(text = client, fontSize = 18.sp, fontWeight = Bold, color = colorScheme.onSecondaryContainer)
Text(text = session.info, fontSize = 16.sp, modifier = Modifier.alpha(0.7f), color = colorScheme.onSecondaryContainer)
Text(text = session.location, fontSize = 16.sp, modifier = Modifier.alpha(0.7f), color = colorScheme.onSecondaryContainer)
}
Text(text = time, fontSize = 16.sp, color = colorScheme.onSecondaryContainer)
}
}
}

View File

@@ -0,0 +1,433 @@
package com.sffteam.voidclient.preferences
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
import androidx.compose.material.icons.filled.PhotoCamera
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.sffteam.voidclient.AccountManager
import com.sffteam.voidclient.ChatManager
import com.sffteam.voidclient.MainActivity
import com.sffteam.voidclient.OPCode
import com.sffteam.voidclient.SocketManager
import com.sffteam.voidclient.UserManager
import com.sffteam.voidclient.Utils
import com.sffteam.voidclient.ui.theme.AppTheme
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import io.ktor.client.statement.request
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import java.util.Locale.getDefault
class ProfileSettingsActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
AppTheme {
Scaffold(
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = colorScheme.surfaceContainer,
),
title = {
Text("Профиль")
},
navigationIcon = {
IconButton({ finish() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBackIos,
contentDescription = "Меню"
)
}
},
)
}) {
Column(
modifier = Modifier
.padding(it)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(15.dp)
) {
val users by UserManager.usersList.collectAsState()
val you = users[AccountManager.accountID]
val firstName = remember { mutableStateOf(you!!.firstName) }
val lastName = remember { mutableStateOf(you?.lastName ?: "") }
val desc = remember { mutableStateOf(you?.description) }
val context = LocalContext.current
var selectedImages by remember {
mutableStateOf<List<Uri?>>(emptyList())
}
val singlePhotoPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
println("uris $uri")
selectedImages = listOf(uri)
}
)
Box(
modifier = Modifier.clickable {
singlePhotoPickerLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}, contentAlignment = Alignment.Center
) {
if (you?.avatarUrl?.isNotEmpty() == true) {
AsyncImage(
model = you.avatarUrl,
contentDescription = "ChatIcon",
modifier = Modifier
.width(100.dp)
.height(100.dp)
.clip(CircleShape)
.align(Alignment.Center),
contentScale = ContentScale.Crop,
)
} else {
val fullName = you?.firstName + you?.lastName
val initial =
fullName.split(" ").mapNotNull { it.firstOrNull() }.take(2)
.joinToString("").uppercase(getDefault())
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.width(100.dp)
.height(100.dp)
.clip(CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(
Utils.getColorForAvatar(fullName).first,
Utils.getColorForAvatar(fullName).second
)
)
),
) {
Text(
text = initial,
color = Color.White,
style = MaterialTheme.typography.labelLarge,
fontSize = 25.sp
)
}
}
Box(
modifier = Modifier
.background(
colorScheme.primaryContainer,
shape = RoundedCornerShape(8.dp)
)
.align(Alignment.BottomEnd),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Filled.PhotoCamera,
contentDescription = "Меню",
modifier = Modifier
.size(30.dp)
.align(Alignment.Center),
tint = colorScheme.onPrimaryContainer
)
}
}
OutlinedTextField(
value = firstName.value,
onValueChange = { newText ->
if (newText.length <= 59) {
firstName.value = newText
}
},
label = { Text("Имя") },
textStyle = TextStyle(fontSize = 25.sp),
modifier = Modifier.align(Alignment.CenterHorizontally),
)
OutlinedTextField(
value = lastName.value,
onValueChange = { newText ->
if (newText.length <= 59) {
lastName.value = newText
}
},
label = { Text("Фамилия") },
textStyle = TextStyle(fontSize = 25.sp),
modifier = Modifier.align(Alignment.CenterHorizontally)
)
OutlinedTextField(
value = desc.value.toString(),
onValueChange = { newText ->
if (newText.length <= 400) {
desc.value = newText
}
},
label = { Text("О себе") },
textStyle = TextStyle(fontSize = 25.sp),
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Button(
onClick = {
if (selectedImages.isNotEmpty()) {
println("mr")
var uploadedImages = mapOf<String, JsonElement>()
var imageType = ""
var imageName = ""
val cursor = context.contentResolver.query(
selectedImages.last()!!, null, null, null, null
)
cursor?.use {
if (it.moveToFirst()) {
val nameIndex =
it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
imageName = it.getString(nameIndex)
}
}
val packet = SocketManager.packPacket(
OPCode.UPLOAD_IMAGE.opcode, JsonObject(
mapOf(
"count" to JsonPrimitive(1)
)
)
)
val client = HttpClient(CIO)
runBlocking {
println("pen")
val imageBytes = try {
context.contentResolver.openInputStream(selectedImages.last()!!)
?.use { inputStream ->
inputStream.readBytes()
}
} catch (e: Exception) {
null
}
SocketManager.sendPacket(packet, { packet ->
if (packet.payload is JsonObject) {
runBlocking {
try {
val response: HttpResponse =
client.post(packet.payload["url"]?.jsonPrimitive?.content.toString()) {
method = HttpMethod.Post
headers {
append(
HttpHeaders.UserAgent,
"OKMessages/25.12.1 (Android 14; oneplus CPH2465; 382dpi 2300x1023)"
)
append(
HttpHeaders.ContentType,
"application/octet-stream"
)
append(
HttpHeaders.ContentDisposition,
"attachment; filename=${imageName}"
)
append(
"X-Uploading-Mode",
"parallel"
)
append(
"Content-Range",
"bytes 0-${imageBytes!!.size - 1}/${imageBytes.size}"
)
append(
HttpHeaders.Connection,
"keep-alive"
)
append(
HttpHeaders.AcceptEncoding,
"gzip"
)
}
setBody(imageBytes)
}
println(response.request.content)
println("Upload response status: ${response.status}")
val content =
Json.parseToJsonElement(response.bodyAsText())
uploadedImages =
content.jsonObject["photos"]!!.jsonObject
print(content)
println("is")
var packetJson = mutableMapOf(
"firstName" to JsonPrimitive(firstName.value),
"lastName" to JsonPrimitive(lastName.value),
)
packetJson["description"] =
JsonPrimitive(desc.value)
packetJson["avatarType"] =
JsonPrimitive("USER_AVATAR")
packetJson["photoToken"] = JsonPrimitive(
uploadedImages.toList()
.last().second.jsonObject["token"]!!.jsonPrimitive.content
)
val packet = SocketManager.packPacket(
OPCode.CHANGE_PROFILE.opcode,
JsonObject(packetJson)
)
println("gay")
GlobalScope.launch {
SocketManager.sendPacket(
packet, { packet ->
if (packet.payload is JsonObject) {
AccountManager.accountID =
packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["id"]!!.jsonPrimitive.long
AccountManager.phone =
packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["phone"]!!.jsonPrimitive.content
}
})
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
client.close()
}
}
}
})
}
} else {
var packetJson = mutableMapOf(
"firstName" to JsonPrimitive(firstName.value),
"lastName" to JsonPrimitive(lastName.value),
)
packetJson["description"] = JsonPrimitive(desc.value)
val packet = SocketManager.packPacket(
OPCode.CHANGE_PROFILE.opcode, JsonObject(packetJson)
)
GlobalScope.launch {
SocketManager.sendPacket(packet, { packet ->
if (packet.payload is JsonObject) {
AccountManager.accountID =
packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["id"]!!.jsonPrimitive.long
AccountManager.phone =
packet.payload.jsonObject["profile"]!!.jsonObject["contact"]!!.jsonObject["phone"]!!.jsonPrimitive.content
}
})
}
}
}) {
Text("Сохранить", fontSize = 18.sp)
}
Button(
onClick = {
val packet = SocketManager.packPacket(
OPCode.LOGOUT.opcode, JsonObject(mapOf())
)
val intent: Intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
coroutineScope.launch {
SocketManager.sendPacket(packet, {
})
}
ChatManager.clearChatsList()
UserManager.clearUsersList()
context.startActivity(intent)
finish()
}) {
Text("Выйти из профиля", fontSize = 18.sp)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,409 @@
package com.sffteam.voidclient.preferences
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import com.sffteam.voidclient.AccountManager
import com.sffteam.voidclient.OPCode
import com.sffteam.voidclient.SocketManager
import com.sffteam.voidclient.ui.theme.AppTheme
import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
class SecurityActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val forSettings = mapOf(
"HIDDEN" to mapOf("Контакты" to false, "Никто" to true),
"CONTENT_LEVEL_ACCESS" to mapOf("Весь" to false, "Безопасный" to true),
"CHATS_INVITE" to mapOf("Все" to "ALL", "Контакты" to "CONTACTS"),
"SEARCH_BY_PHONE" to mapOf("Все" to "ALL", "Контакты" to "CONTACTS"),
"INCOMING_CALL" to mapOf("Все" to "ALL", "Контакты" to "CONTACTS"),
)
setContent {
val coroutineScope = rememberCoroutineScope()
val settings by AccountManager.settings.collectAsState()
val sheetState = rememberModalBottomSheetState()
val safeMode = remember { mutableStateOf(settings.safeMode) }
val context = LocalContext.current
var showBottomSheet by remember { mutableStateOf(false) }
val selectedSettings = remember { mutableStateOf("") }
if (AccountManager.sessionsList.value.isEmpty()) {
val packet =
SocketManager.packPacket(OPCode.SESSIONS.opcode, JsonObject(emptyMap()))
coroutineScope.launch {
SocketManager.sendPacket(packet, { packet ->
if (packet.payload is JsonObject) {
AccountManager.processSession(packet.payload["sessions"]!!.jsonArray)
}
})
}
}
AppTheme {
LaunchedEffect(settings) {
safeMode.value = settings.safeMode
}
if (showBottomSheet) {
ModalBottomSheet(
onDismissRequest = {
showBottomSheet = false
}, sheetState = sheetState
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(start = 8.dp)
) {
forSettings[selectedSettings.value]?.forEach { (index, value) ->
Text(index, modifier = Modifier.clickable {
val packet = SocketManager.packPacket(OPCode.SETTINGS_CHANGE.opcode, JsonObject(
mapOf(
"settings" to JsonObject(
mapOf(
"user" to JsonObject(
mapOf(
if (value is String) {
selectedSettings.value to JsonPrimitive(value)
} else if (value is Boolean) {
selectedSettings.value to JsonPrimitive(value)
} else {
selectedSettings.value to JsonPrimitive("")
}
)
)
)
)
)
))
coroutineScope.launch {
SocketManager.sendPacket(packet, { packet ->
if (packet.payload is JsonObject) {
AccountManager.processSettings(packet.payload["user"]!!.jsonObject)
}
})
}
showBottomSheet = false
}
.padding(8.dp), fontSize = 24.sp)
}
}
}
}
Scaffold(
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = colorScheme.surfaceContainer,
),
title = {
Text("Безопасность")
},
navigationIcon = {
IconButton({ finish() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBackIos,
contentDescription = "Меню"
)
}
},
)
}) {
Box(
modifier = Modifier
.padding(it)
.fillMaxSize()
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
item() {
Box(
modifier = Modifier
.background(
colorScheme.secondaryContainer,
shape = RoundedCornerShape(20.dp)
)
.fillMaxWidth()
.padding(start = 4.dp, top = 4.dp, end = 4.dp)
) {
Column(modifier = Modifier
.padding(bottom = 8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Outlined.Lock,
contentDescription = "lol",
modifier = Modifier.size(40.dp).padding(start = 8.dp)
)
Text("Безопасный режим", fontSize = 24.sp, modifier = Modifier.padding(start = 8.dp))
Spacer(modifier = Modifier.weight(1f))
Switch(
checked = safeMode.value,
onCheckedChange = {
val packet = SocketManager.packPacket(OPCode.SETTINGS_CHANGE.opcode, JsonObject(
mapOf(
"settings" to JsonObject(
mapOf(
"user" to JsonObject(
mapOf(
"SAFE_MODE" to JsonPrimitive(it)
)
)
)
)
)
))
coroutineScope.launch {
SocketManager.sendPacket(packet, { packet ->
if (packet.payload is JsonObject) {
AccountManager.processSettings(packet.payload["user"]!!.jsonObject)
}
})
}
},
modifier = Modifier.padding(end = 4.dp)
)
}
Row(modifier = Modifier
.clickable {
if (settings.safeMode) {
Toast.makeText(
context,
"Отключите безопасный режим, чтобы изменить эту настройку",
Toast.LENGTH_SHORT
).show()
} else {
selectedSettings.value = "SEARCH_BY_PHONE"
showBottomSheet = true
}
}) {
Text("Найти меня по номеру", fontSize = 20.sp, modifier = Modifier.padding(start = 8.dp))
val whoCan = if (settings.searchByPhone == "ALL") {
"Все"
} else {
"Контакты"
}
Spacer(modifier = Modifier.weight(1f))
if (settings.safeMode) {
Icon(
Icons.Outlined.Lock,
contentDescription = "lol",
modifier = Modifier.size(25.dp).padding(start = 4.dp, end = 4.dp)
)
}
Text(whoCan, fontSize = 20.sp, modifier = Modifier.padding(end = 8.dp))
}
Row(modifier = Modifier
.clickable {
if (settings.safeMode) {
Toast.makeText(
context,
"Отключите безопасный режим, чтобы изменить эту настройку",
Toast.LENGTH_SHORT
).show()
} else {
selectedSettings.value = "INCOMING_CALL"
showBottomSheet = true
}
}) {
Text("Позвонить", fontSize = 20.sp, modifier = Modifier.padding(start = 8.dp))
val whoCan = if (settings.incomingCall == "ALL") {
"Все"
} else {
"Контакты"
}
Spacer(modifier = Modifier.weight(1f))
if (settings.safeMode) {
Icon(
Icons.Outlined.Lock,
contentDescription = "lol",
modifier = Modifier.size(25.dp).padding(start = 4.dp, end = 4.dp)
)
}
Text(whoCan, fontSize = 20.sp, modifier = Modifier.padding(end = 8.dp))
}
Row(modifier = Modifier
.clickable {
if (settings.safeMode) {
Toast.makeText(
context,
"Отключите безопасный режим, чтобы изменить эту настройку",
Toast.LENGTH_SHORT
).show()
} else {
selectedSettings.value = "CHATS_INVITE"
showBottomSheet = true
}
}) {
Text("Приглашения в чат", fontSize = 20.sp, modifier = Modifier.padding(start = 8.dp))
val whoCan = if (settings.chatsInvite == "ALL") {
"Все"
} else {
"Контакты"
}
Spacer(modifier = Modifier.weight(1f))
if (settings.safeMode) {
Icon(
Icons.Outlined.Lock,
contentDescription = "lol",
modifier = Modifier.size(25.dp).padding(start = 4.dp, end = 4.dp)
)
}
Text(whoCan, fontSize = 20.sp, modifier = Modifier.padding(end = 8.dp))
}
Row(modifier = Modifier
.clickable {
if (settings.safeMode) {
Toast.makeText(
context,
"Отключите безопасный режим, чтобы изменить эту настройку",
Toast.LENGTH_SHORT
).show()
} else {
selectedSettings.value = "CONTENT_LEVEL_ACCESS"
showBottomSheet = true
}
}) {
Text("Показывать контент", fontSize = 20.sp, modifier = Modifier.padding(start = 8.dp))
val content = if (!settings.contentLevelAccess) {
"Весь"
} else {
"Безопасный"
}
Spacer(modifier = Modifier.weight(1f))
if (settings.safeMode) {
Icon(
Icons.Outlined.Lock,
contentDescription = "lol",
modifier = Modifier.size(25.dp).padding(start = 4.dp, end = 4.dp)
)
}
Text(content, fontSize = 20.sp, modifier = Modifier.padding(end = 8.dp))
}
}
}
}
item() {
Box(
modifier = Modifier
.background(
colorScheme.secondaryContainer,
shape = RoundedCornerShape(20.dp)
)
.fillMaxWidth()
.padding(start = 4.dp, top = 4.dp, end = 4.dp)
) {
Column() {
Text(
"Информация",
fontSize = 22.sp,
color = colorScheme.primary,
modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)
)
Row(modifier = Modifier
.clickable {
selectedSettings.value = "HIDDEN"
showBottomSheet = true
}
.padding(bottom = 8.dp)) {
Text("Статус \"В сети\"", fontSize = 20.sp, modifier = Modifier.padding(start = 8.dp))
val hidden = if (settings.hidden) {
"Никто"
} else {
"Контакты"
}
Spacer(modifier = Modifier.weight(1f))
Text(hidden, fontSize = 20.sp, modifier = Modifier.padding(end = 8.dp))
}
}
}
}
item() {}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,515 @@
package com.sffteam.voidclient.preferences
import android.content.ClipData
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.outlined.ChatBubble
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.Smartphone
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.sffteam.voidclient.AccountManager
import com.sffteam.voidclient.ImageViewerActivity
import com.sffteam.voidclient.UserManager
import com.sffteam.voidclient.Utils
import com.sffteam.voidclient.ui.theme.AppTheme
import kotlinx.coroutines.launch
import java.util.Locale.getDefault
class SettingsActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AppTheme {
Scaffold(
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = colorScheme.surfaceContainer,
),
title = {
Text("Настройки")
},
navigationIcon = {
IconButton({ finish() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBackIos,
contentDescription = "Меню"
)
}
},
)
}) {
val context = LocalContext.current
val users by UserManager.usersList.collectAsState()
println("users $users")
val user = users[AccountManager.accountID]
val username = user?.firstName + if (user?.lastName?.isNotEmpty() == true) {
" " + user.lastName
} else {
""
}
val coroutineScope = rememberCoroutineScope()
val clipboardManager = LocalClipboard.current
Box(
modifier = Modifier
.padding(it)
.fillMaxSize()
) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(top = 12.dp, start = 12.dp, end = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
// Account
item {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
if (user?.avatarUrl?.isNotEmpty() == true) {
AsyncImage(
model = user.avatarUrl,
contentDescription = "ChatIcon",
modifier = Modifier
.width(100.dp)
.height(100.dp)
.clip(CircleShape)
.align(Alignment.CenterHorizontally)
.clickable {
val intent = Intent(context, ImageViewerActivity::class.java)
intent.putExtra("isSingleImage", true)
intent.putExtra("image", user.avatarUrl)
context.startActivity(intent)
},
contentScale = ContentScale.Crop,
)
} else {
val fullName = user?.firstName + user?.lastName
val initial =
fullName.split(" ").mapNotNull { it.firstOrNull() }
.take(2).joinToString("").uppercase(getDefault())
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.width(100.dp)
.height(100.dp)
.clip(CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(
Utils.getColorForAvatar(fullName).first,
Utils.getColorForAvatar(fullName).second
)
)
)
.align(Alignment.CenterHorizontally),
) {
Text(
text = initial,
color = Color.White,
style = MaterialTheme.typography.labelLarge,
fontSize = 25.sp
)
}
}
Text(
text = username,
fontSize = 20.sp,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.clickable {
coroutineScope.launch {
clipboardManager.setClipEntry(
ClipEntry(
ClipData.newPlainText(
username, username
)
)
)
}
}
)
Text(
text = "+${AccountManager.phone}",
fontSize = 16.sp,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.clickable {
coroutineScope.launch {
clipboardManager.setClipEntry(
ClipEntry(
ClipData.newPlainText(
"+${AccountManager.phone}",
"+${AccountManager.phone}"
)
)
)
}
}
)
Text(
text = "ID: ${AccountManager.accountID}",
fontSize = 16.sp,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.clickable {
coroutineScope.launch {
clipboardManager.setClipEntry(
ClipEntry(
ClipData.newPlainText(
"${AccountManager.accountID}",
"${AccountManager.accountID}"
)
)
)
}
})
Text(
text = "Нажмите на информацию, чтобы скопировать её",
fontSize = 14.sp,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.alpha(0.7f)
)
}
}
item {
Box(
modifier = Modifier
.background(
colorScheme.secondaryContainer,
shape = RoundedCornerShape(20.dp)
)
.fillMaxWidth()
.clickable {
val intent =
Intent(context, ProfileSettingsActivity::class.java)
context.startActivity(intent)
}) {
Column() {
Row(
modifier = Modifier.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
Icons.Filled.AccountCircle,
"lol",
modifier = Modifier
.size(30.dp)
.padding(),
tint = colorScheme.onSecondaryContainer
)
Text(
"Мой аккаунт",
fontSize = 20.sp,
modifier = Modifier.align(Alignment.CenterVertically),
color = colorScheme.onSecondaryContainer
)
}
}
}
}
// Settings
item {
Box(
modifier = Modifier
.background(
colorScheme.secondaryContainer,
shape = RoundedCornerShape(20.dp)
)
.fillMaxWidth()
) {
Column() {
// For next update
// Row(
// modifier = Modifier
// .padding(12.dp)
// .clickable {
// val intent =
// Intent(
// context,
// ProfileSettingsActivity::class.java
// )
//
// context.startActivity(intent)
// },
// horizontalArrangement = Arrangement.spacedBy(6.dp)
// ) {
// Icon(
// Icons.Outlined.Settings,
// "lol",
// modifier = Modifier
// .size(25.dp)
// .padding()
// )
// Text(
// "Настройки Open MAX", fontSize = 20.sp,
// modifier = Modifier.align(Alignment.CenterVertically)
// )
// }
// Row(
// modifier = Modifier
// .padding(12.dp)
// .clickable {
// val intent =
// Intent(
// context,
// ChatSettingsActivity::class.java
// )
//
// context.startActivity(intent)
// },
// horizontalArrangement = Arrangement.spacedBy(6.dp)
// ) {
// Icon(
// Icons.Outlined.ChatBubble,
// "lol",
// modifier = Modifier
// .size(25.dp)
// .padding()
// )
// Text(
// "Настройки чатов", fontSize = 20.sp,
// modifier = Modifier.align(Alignment.CenterVertically)
// )
// }
Row(
modifier = Modifier
.padding(12.dp)
.clickable {
val intent =
Intent(
context,
SecurityActivity::class.java
)
context.startActivity(intent)
},
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
Icons.Outlined.Lock,
"lol",
modifier = Modifier
.size(25.dp)
.padding(),
tint = colorScheme.onSecondaryContainer
)
Text(
"Безопасность", fontSize = 20.sp,
modifier = Modifier.align(Alignment.CenterVertically),
color = colorScheme.onSecondaryContainer
)
}
Row(
modifier = Modifier
.padding(12.dp)
.clickable {
val intent =
Intent(context, DevicesActivity::class.java)
context.startActivity(intent)
},
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
Icons.Outlined.Smartphone,
"lol",
modifier = Modifier
.size(25.dp)
.padding(),
tint = colorScheme.onSecondaryContainer
)
Text(
"Устройства",
fontSize = 20.sp,
modifier = Modifier.align(Alignment.CenterVertically),
color = colorScheme.onSecondaryContainer
)
}
Row(
modifier = Modifier
.padding(12.dp)
.clickable {
Toast.makeText(
context,
"Будет доступно в следующих обновлениях",
Toast.LENGTH_LONG
).show()
},
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
Icons.Outlined.Notifications,
"lol",
modifier = Modifier
.size(25.dp)
.padding()
.alpha(0.7f),
tint = colorScheme.onSecondaryContainer
)
Text(
"Уведомления",
fontSize = 20.sp,
modifier = Modifier
.align(Alignment.CenterVertically)
.alpha(0.7f),
color = colorScheme.onSecondaryContainer
)
}
Row(
modifier = Modifier
.padding(12.dp)
.clickable {
Toast.makeText(
context,
"Будет доступно в следующих обновлениях",
Toast.LENGTH_LONG
).show()
},
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
Icons.Outlined.Folder,
"lol",
modifier = Modifier
.size(25.dp)
.padding()
.alpha(0.7f),
tint = colorScheme.onSecondaryContainer
)
Text(
"Папки с чатами",
fontSize = 20.sp,
modifier = Modifier
.align(Alignment.CenterVertically)
.alpha(0.7f),
color = colorScheme.onSecondaryContainer
)
}
}
}
}
item {
Box(
modifier = Modifier
.background(
colorScheme.secondaryContainer,
shape = RoundedCornerShape(20.dp)
)
.fillMaxWidth()
) {
Column() {
Row(
modifier = Modifier
.padding(12.dp)
.clickable {
val intent =
Intent(context, AboutActivity::class.java)
context.startActivity(intent)
},
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
Icons.Outlined.Info,
"lol",
modifier = Modifier
.size(25.dp)
.padding(),
tint = colorScheme.onSecondaryContainer
)
Text(
"О приложении",
fontSize = 20.sp,
modifier = Modifier.align(Alignment.CenterVertically),
color = colorScheme.onSecondaryContainer
)
}
}
}
}
}
Text(
"Void Client a1.0.0",
fontSize = 14.sp,
modifier = Modifier
.alpha(0.7f)
.align(Alignment.BottomCenter)
)
}
}
}
}
}
}

View File

@@ -0,0 +1,225 @@
package com.sffteam.voidclient.ui.theme
import androidx.compose.ui.graphics.Color
val primaryLight = Color(0xFF415F91)
val onPrimaryLight = Color(0xFFFFFFFF)
val primaryContainerLight = Color(0xFFD6E3FF)
val onPrimaryContainerLight = Color(0xFF284777)
val secondaryLight = Color(0xFF565F71)
val onSecondaryLight = Color(0xFFFFFFFF)
val secondaryContainerLight = Color(0xFFDAE2F9)
val onSecondaryContainerLight = Color(0xFF3E4759)
val tertiaryLight = Color(0xFF705575)
val onTertiaryLight = Color(0xFFFFFFFF)
val tertiaryContainerLight = Color(0xFFFAD8FD)
val onTertiaryContainerLight = Color(0xFF573E5C)
val errorLight = Color(0xFFBA1A1A)
val onErrorLight = Color(0xFFFFFFFF)
val errorContainerLight = Color(0xFFFFDAD6)
val onErrorContainerLight = Color(0xFF93000A)
val backgroundLight = Color(0xFFF9F9FF)
val onBackgroundLight = Color(0xFF191C20)
val surfaceLight = Color(0xFFF9F9FF)
val onSurfaceLight = Color(0xFF191C20)
val surfaceVariantLight = Color(0xFFE0E2EC)
val onSurfaceVariantLight = Color(0xFF44474E)
val outlineLight = Color(0xFF74777F)
val outlineVariantLight = Color(0xFFC4C6D0)
val scrimLight = Color(0xFF000000)
val inverseSurfaceLight = Color(0xFF2E3036)
val inverseOnSurfaceLight = Color(0xFFF0F0F7)
val inversePrimaryLight = Color(0xFFAAC7FF)
val surfaceDimLight = Color(0xFFD9D9E0)
val surfaceBrightLight = Color(0xFFF9F9FF)
val surfaceContainerLowestLight = Color(0xFFFFFFFF)
val surfaceContainerLowLight = Color(0xFFF3F3FA)
val surfaceContainerLight = Color(0xFFEDEDF4)
val surfaceContainerHighLight = Color(0xFFE7E8EE)
val surfaceContainerHighestLight = Color(0xFFE2E2E9)
val primaryLightMediumContrast = Color(0xFF133665)
val onPrimaryLightMediumContrast = Color(0xFFFFFFFF)
val primaryContainerLightMediumContrast = Color(0xFF506DA0)
val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)
val secondaryLightMediumContrast = Color(0xFF2E3647)
val onSecondaryLightMediumContrast = Color(0xFFFFFFFF)
val secondaryContainerLightMediumContrast = Color(0xFF646D80)
val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)
val tertiaryLightMediumContrast = Color(0xFF452E4A)
val onTertiaryLightMediumContrast = Color(0xFFFFFFFF)
val tertiaryContainerLightMediumContrast = Color(0xFF7F6484)
val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)
val errorLightMediumContrast = Color(0xFF740006)
val onErrorLightMediumContrast = Color(0xFFFFFFFF)
val errorContainerLightMediumContrast = Color(0xFFCF2C27)
val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)
val backgroundLightMediumContrast = Color(0xFFF9F9FF)
val onBackgroundLightMediumContrast = Color(0xFF191C20)
val surfaceLightMediumContrast = Color(0xFFF9F9FF)
val onSurfaceLightMediumContrast = Color(0xFF0F1116)
val surfaceVariantLightMediumContrast = Color(0xFFE0E2EC)
val onSurfaceVariantLightMediumContrast = Color(0xFF33363E)
val outlineLightMediumContrast = Color(0xFF4F525A)
val outlineVariantLightMediumContrast = Color(0xFF6A6D75)
val scrimLightMediumContrast = Color(0xFF000000)
val inverseSurfaceLightMediumContrast = Color(0xFF2E3036)
val inverseOnSurfaceLightMediumContrast = Color(0xFFF0F0F7)
val inversePrimaryLightMediumContrast = Color(0xFFAAC7FF)
val surfaceDimLightMediumContrast = Color(0xFFC5C6CD)
val surfaceBrightLightMediumContrast = Color(0xFFF9F9FF)
val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)
val surfaceContainerLowLightMediumContrast = Color(0xFFF3F3FA)
val surfaceContainerLightMediumContrast = Color(0xFFE7E8EE)
val surfaceContainerHighLightMediumContrast = Color(0xFFDCDCE3)
val surfaceContainerHighestLightMediumContrast = Color(0xFFD1D1D8)
val primaryLightHighContrast = Color(0xFF032B5B)
val onPrimaryLightHighContrast = Color(0xFFFFFFFF)
val primaryContainerLightHighContrast = Color(0xFF2A497A)
val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)
val secondaryLightHighContrast = Color(0xFF232C3D)
val onSecondaryLightHighContrast = Color(0xFFFFFFFF)
val secondaryContainerLightHighContrast = Color(0xFF41495B)
val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)
val tertiaryLightHighContrast = Color(0xFF3A2440)
val onTertiaryLightHighContrast = Color(0xFFFFFFFF)
val tertiaryContainerLightHighContrast = Color(0xFF59405E)
val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)
val errorLightHighContrast = Color(0xFF600004)
val onErrorLightHighContrast = Color(0xFFFFFFFF)
val errorContainerLightHighContrast = Color(0xFF98000A)
val onErrorContainerLightHighContrast = Color(0xFFFFFFFF)
val backgroundLightHighContrast = Color(0xFFF9F9FF)
val onBackgroundLightHighContrast = Color(0xFF191C20)
val surfaceLightHighContrast = Color(0xFFF9F9FF)
val onSurfaceLightHighContrast = Color(0xFF000000)
val surfaceVariantLightHighContrast = Color(0xFFE0E2EC)
val onSurfaceVariantLightHighContrast = Color(0xFF000000)
val outlineLightHighContrast = Color(0xFF292C33)
val outlineVariantLightHighContrast = Color(0xFF464951)
val scrimLightHighContrast = Color(0xFF000000)
val inverseSurfaceLightHighContrast = Color(0xFF2E3036)
val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)
val inversePrimaryLightHighContrast = Color(0xFFAAC7FF)
val surfaceDimLightHighContrast = Color(0xFFB8B8BF)
val surfaceBrightLightHighContrast = Color(0xFFF9F9FF)
val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)
val surfaceContainerLowLightHighContrast = Color(0xFFF0F0F7)
val surfaceContainerLightHighContrast = Color(0xFFE2E2E9)
val surfaceContainerHighLightHighContrast = Color(0xFFD3D4DB)
val surfaceContainerHighestLightHighContrast = Color(0xFFC5C6CD)
val primaryDark = Color(0xFFAAC7FF)
val onPrimaryDark = Color(0xFF0A305F)
val primaryContainerDark = Color(0xFF284777)
val onPrimaryContainerDark = Color(0xFFD6E3FF)
val secondaryDark = Color(0xFFBEC6DC)
val onSecondaryDark = Color(0xFF283141)
val secondaryContainerDark = Color(0xFF3E4759)
val onSecondaryContainerDark = Color(0xFFDAE2F9)
val tertiaryDark = Color(0xFFDDBCE0)
val onTertiaryDark = Color(0xFF3F2844)
val tertiaryContainerDark = Color(0xFF573E5C)
val onTertiaryContainerDark = Color(0xFFFAD8FD)
val errorDark = Color(0xFFFFB4AB)
val onErrorDark = Color(0xFF690005)
val errorContainerDark = Color(0xFF93000A)
val onErrorContainerDark = Color(0xFFFFDAD6)
val backgroundDark = Color(0xFF111318)
val onBackgroundDark = Color(0xFFE2E2E9)
val surfaceDark = Color(0xFF111318)
val onSurfaceDark = Color(0xFFE2E2E9)
val surfaceVariantDark = Color(0xFF44474E)
val onSurfaceVariantDark = Color(0xFFC4C6D0)
val outlineDark = Color(0xFF8E9099)
val outlineVariantDark = Color(0xFF44474E)
val scrimDark = Color(0xFF000000)
val inverseSurfaceDark = Color(0xFFE2E2E9)
val inverseOnSurfaceDark = Color(0xFF2E3036)
val inversePrimaryDark = Color(0xFF415F91)
val surfaceDimDark = Color(0xFF111318)
val surfaceBrightDark = Color(0xFF37393E)
val surfaceContainerLowestDark = Color(0xFF0C0E13)
val surfaceContainerLowDark = Color(0xFF191C20)
val surfaceContainerDark = Color(0xFF1D2024)
val surfaceContainerHighDark = Color(0xFF282A2F)
val surfaceContainerHighestDark = Color(0xFF33353A)
val primaryDarkMediumContrast = Color(0xFFCDDDFF)
val onPrimaryDarkMediumContrast = Color(0xFF002551)
val primaryContainerDarkMediumContrast = Color(0xFF7491C7)
val onPrimaryContainerDarkMediumContrast = Color(0xFF000000)
val secondaryDarkMediumContrast = Color(0xFFD4DCF2)
val onSecondaryDarkMediumContrast = Color(0xFF1D2636)
val secondaryContainerDarkMediumContrast = Color(0xFF8891A5)
val onSecondaryContainerDarkMediumContrast = Color(0xFF000000)
val tertiaryDarkMediumContrast = Color(0xFFF3D2F7)
val onTertiaryDarkMediumContrast = Color(0xFF331D39)
val tertiaryContainerDarkMediumContrast = Color(0xFFA487A9)
val onTertiaryContainerDarkMediumContrast = Color(0xFF000000)
val errorDarkMediumContrast = Color(0xFFFFD2CC)
val onErrorDarkMediumContrast = Color(0xFF540003)
val errorContainerDarkMediumContrast = Color(0xFFFF5449)
val onErrorContainerDarkMediumContrast = Color(0xFF000000)
val backgroundDarkMediumContrast = Color(0xFF111318)
val onBackgroundDarkMediumContrast = Color(0xFFE2E2E9)
val surfaceDarkMediumContrast = Color(0xFF111318)
val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF)
val surfaceVariantDarkMediumContrast = Color(0xFF44474E)
val onSurfaceVariantDarkMediumContrast = Color(0xFFDADCE6)
val outlineDarkMediumContrast = Color(0xFFAFB2BB)
val outlineVariantDarkMediumContrast = Color(0xFF8E9099)
val scrimDarkMediumContrast = Color(0xFF000000)
val inverseSurfaceDarkMediumContrast = Color(0xFFE2E2E9)
val inverseOnSurfaceDarkMediumContrast = Color(0xFF282A2F)
val inversePrimaryDarkMediumContrast = Color(0xFF294878)
val surfaceDimDarkMediumContrast = Color(0xFF111318)
val surfaceBrightDarkMediumContrast = Color(0xFF43444A)
val surfaceContainerLowestDarkMediumContrast = Color(0xFF06070C)
val surfaceContainerLowDarkMediumContrast = Color(0xFF1B1E22)
val surfaceContainerDarkMediumContrast = Color(0xFF26282D)
val surfaceContainerHighDarkMediumContrast = Color(0xFF313238)
val surfaceContainerHighestDarkMediumContrast = Color(0xFF3C3E43)
val primaryDarkHighContrast = Color(0xFFEBF0FF)
val onPrimaryDarkHighContrast = Color(0xFF000000)
val primaryContainerDarkHighContrast = Color(0xFFA6C3FC)
val onPrimaryContainerDarkHighContrast = Color(0xFF000B20)
val secondaryDarkHighContrast = Color(0xFFEBF0FF)
val onSecondaryDarkHighContrast = Color(0xFF000000)
val secondaryContainerDarkHighContrast = Color(0xFFBAC3D8)
val onSecondaryContainerDarkHighContrast = Color(0xFF030B1A)
val tertiaryDarkHighContrast = Color(0xFFFFE9FF)
val onTertiaryDarkHighContrast = Color(0xFF000000)
val tertiaryContainerDarkHighContrast = Color(0xFFD8B8DC)
val onTertiaryContainerDarkHighContrast = Color(0xFF16041D)
val errorDarkHighContrast = Color(0xFFFFECE9)
val onErrorDarkHighContrast = Color(0xFF000000)
val errorContainerDarkHighContrast = Color(0xFFFFAEA4)
val onErrorContainerDarkHighContrast = Color(0xFF220001)
val backgroundDarkHighContrast = Color(0xFF111318)
val onBackgroundDarkHighContrast = Color(0xFFE2E2E9)
val surfaceDarkHighContrast = Color(0xFF111318)
val onSurfaceDarkHighContrast = Color(0xFFFFFFFF)
val surfaceVariantDarkHighContrast = Color(0xFF44474E)
val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF)
val outlineDarkHighContrast = Color(0xFFEEEFF9)
val outlineVariantDarkHighContrast = Color(0xFFC0C2CC)
val scrimDarkHighContrast = Color(0xFF000000)
val inverseSurfaceDarkHighContrast = Color(0xFFE2E2E9)
val inverseOnSurfaceDarkHighContrast = Color(0xFF000000)
val inversePrimaryDarkHighContrast = Color(0xFF294878)
val surfaceDimDarkHighContrast = Color(0xFF111318)
val surfaceBrightDarkHighContrast = Color(0xFF4E5056)
val surfaceContainerLowestDarkHighContrast = Color(0xFF000000)
val surfaceContainerLowDarkHighContrast = Color(0xFF1D2024)
val surfaceContainerDarkHighContrast = Color(0xFF2E3036)
val surfaceContainerHighDarkHighContrast = Color(0xFF393B41)
val surfaceContainerHighestDarkHighContrast = Color(0xFF45474C)

View File

@@ -0,0 +1,280 @@
package com.sffteam.voidclient.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
private val lightScheme = lightColorScheme(
primary = primaryLight,
onPrimary = onPrimaryLight,
primaryContainer = primaryContainerLight,
onPrimaryContainer = onPrimaryContainerLight,
secondary = secondaryLight,
onSecondary = onSecondaryLight,
secondaryContainer = secondaryContainerLight,
onSecondaryContainer = onSecondaryContainerLight,
tertiary = tertiaryLight,
onTertiary = onTertiaryLight,
tertiaryContainer = tertiaryContainerLight,
onTertiaryContainer = onTertiaryContainerLight,
error = errorLight,
onError = onErrorLight,
errorContainer = errorContainerLight,
onErrorContainer = onErrorContainerLight,
background = backgroundLight,
onBackground = onBackgroundLight,
surface = surfaceLight,
onSurface = onSurfaceLight,
surfaceVariant = surfaceVariantLight,
onSurfaceVariant = onSurfaceVariantLight,
outline = outlineLight,
outlineVariant = outlineVariantLight,
scrim = scrimLight,
inverseSurface = inverseSurfaceLight,
inverseOnSurface = inverseOnSurfaceLight,
inversePrimary = inversePrimaryLight,
surfaceDim = surfaceDimLight,
surfaceBright = surfaceBrightLight,
surfaceContainerLowest = surfaceContainerLowestLight,
surfaceContainerLow = surfaceContainerLowLight,
surfaceContainer = surfaceContainerLight,
surfaceContainerHigh = surfaceContainerHighLight,
surfaceContainerHighest = surfaceContainerHighestLight,
)
private val darkScheme = darkColorScheme(
primary = primaryDark,
onPrimary = onPrimaryDark,
primaryContainer = primaryContainerDark,
onPrimaryContainer = onPrimaryContainerDark,
secondary = secondaryDark,
onSecondary = onSecondaryDark,
secondaryContainer = secondaryContainerDark,
onSecondaryContainer = onSecondaryContainerDark,
tertiary = tertiaryDark,
onTertiary = onTertiaryDark,
tertiaryContainer = tertiaryContainerDark,
onTertiaryContainer = onTertiaryContainerDark,
error = errorDark,
onError = onErrorDark,
errorContainer = errorContainerDark,
onErrorContainer = onErrorContainerDark,
background = backgroundDark,
onBackground = onBackgroundDark,
surface = surfaceDark,
onSurface = onSurfaceDark,
surfaceVariant = surfaceVariantDark,
onSurfaceVariant = onSurfaceVariantDark,
outline = outlineDark,
outlineVariant = outlineVariantDark,
scrim = scrimDark,
inverseSurface = inverseSurfaceDark,
inverseOnSurface = inverseOnSurfaceDark,
inversePrimary = inversePrimaryDark,
surfaceDim = surfaceDimDark,
surfaceBright = surfaceBrightDark,
surfaceContainerLowest = surfaceContainerLowestDark,
surfaceContainerLow = surfaceContainerLowDark,
surfaceContainer = surfaceContainerDark,
surfaceContainerHigh = surfaceContainerHighDark,
surfaceContainerHighest = surfaceContainerHighestDark,
)
private val mediumContrastLightColorScheme = lightColorScheme(
primary = primaryLightMediumContrast,
onPrimary = onPrimaryLightMediumContrast,
primaryContainer = primaryContainerLightMediumContrast,
onPrimaryContainer = onPrimaryContainerLightMediumContrast,
secondary = secondaryLightMediumContrast,
onSecondary = onSecondaryLightMediumContrast,
secondaryContainer = secondaryContainerLightMediumContrast,
onSecondaryContainer = onSecondaryContainerLightMediumContrast,
tertiary = tertiaryLightMediumContrast,
onTertiary = onTertiaryLightMediumContrast,
tertiaryContainer = tertiaryContainerLightMediumContrast,
onTertiaryContainer = onTertiaryContainerLightMediumContrast,
error = errorLightMediumContrast,
onError = onErrorLightMediumContrast,
errorContainer = errorContainerLightMediumContrast,
onErrorContainer = onErrorContainerLightMediumContrast,
background = backgroundLightMediumContrast,
onBackground = onBackgroundLightMediumContrast,
surface = surfaceLightMediumContrast,
onSurface = onSurfaceLightMediumContrast,
surfaceVariant = surfaceVariantLightMediumContrast,
onSurfaceVariant = onSurfaceVariantLightMediumContrast,
outline = outlineLightMediumContrast,
outlineVariant = outlineVariantLightMediumContrast,
scrim = scrimLightMediumContrast,
inverseSurface = inverseSurfaceLightMediumContrast,
inverseOnSurface = inverseOnSurfaceLightMediumContrast,
inversePrimary = inversePrimaryLightMediumContrast,
surfaceDim = surfaceDimLightMediumContrast,
surfaceBright = surfaceBrightLightMediumContrast,
surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
surfaceContainerLow = surfaceContainerLowLightMediumContrast,
surfaceContainer = surfaceContainerLightMediumContrast,
surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,
)
private val highContrastLightColorScheme = lightColorScheme(
primary = primaryLightHighContrast,
onPrimary = onPrimaryLightHighContrast,
primaryContainer = primaryContainerLightHighContrast,
onPrimaryContainer = onPrimaryContainerLightHighContrast,
secondary = secondaryLightHighContrast,
onSecondary = onSecondaryLightHighContrast,
secondaryContainer = secondaryContainerLightHighContrast,
onSecondaryContainer = onSecondaryContainerLightHighContrast,
tertiary = tertiaryLightHighContrast,
onTertiary = onTertiaryLightHighContrast,
tertiaryContainer = tertiaryContainerLightHighContrast,
onTertiaryContainer = onTertiaryContainerLightHighContrast,
error = errorLightHighContrast,
onError = onErrorLightHighContrast,
errorContainer = errorContainerLightHighContrast,
onErrorContainer = onErrorContainerLightHighContrast,
background = backgroundLightHighContrast,
onBackground = onBackgroundLightHighContrast,
surface = surfaceLightHighContrast,
onSurface = onSurfaceLightHighContrast,
surfaceVariant = surfaceVariantLightHighContrast,
onSurfaceVariant = onSurfaceVariantLightHighContrast,
outline = outlineLightHighContrast,
outlineVariant = outlineVariantLightHighContrast,
scrim = scrimLightHighContrast,
inverseSurface = inverseSurfaceLightHighContrast,
inverseOnSurface = inverseOnSurfaceLightHighContrast,
inversePrimary = inversePrimaryLightHighContrast,
surfaceDim = surfaceDimLightHighContrast,
surfaceBright = surfaceBrightLightHighContrast,
surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
surfaceContainerLow = surfaceContainerLowLightHighContrast,
surfaceContainer = surfaceContainerLightHighContrast,
surfaceContainerHigh = surfaceContainerHighLightHighContrast,
surfaceContainerHighest = surfaceContainerHighestLightHighContrast,
)
private val mediumContrastDarkColorScheme = darkColorScheme(
primary = primaryDarkMediumContrast,
onPrimary = onPrimaryDarkMediumContrast,
primaryContainer = primaryContainerDarkMediumContrast,
onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
secondary = secondaryDarkMediumContrast,
onSecondary = onSecondaryDarkMediumContrast,
secondaryContainer = secondaryContainerDarkMediumContrast,
onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
tertiary = tertiaryDarkMediumContrast,
onTertiary = onTertiaryDarkMediumContrast,
tertiaryContainer = tertiaryContainerDarkMediumContrast,
onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
error = errorDarkMediumContrast,
onError = onErrorDarkMediumContrast,
errorContainer = errorContainerDarkMediumContrast,
onErrorContainer = onErrorContainerDarkMediumContrast,
background = backgroundDarkMediumContrast,
onBackground = onBackgroundDarkMediumContrast,
surface = surfaceDarkMediumContrast,
onSurface = onSurfaceDarkMediumContrast,
surfaceVariant = surfaceVariantDarkMediumContrast,
onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
outline = outlineDarkMediumContrast,
outlineVariant = outlineVariantDarkMediumContrast,
scrim = scrimDarkMediumContrast,
inverseSurface = inverseSurfaceDarkMediumContrast,
inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
inversePrimary = inversePrimaryDarkMediumContrast,
surfaceDim = surfaceDimDarkMediumContrast,
surfaceBright = surfaceBrightDarkMediumContrast,
surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
surfaceContainer = surfaceContainerDarkMediumContrast,
surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,
)
private val highContrastDarkColorScheme = darkColorScheme(
primary = primaryDarkHighContrast,
onPrimary = onPrimaryDarkHighContrast,
primaryContainer = primaryContainerDarkHighContrast,
onPrimaryContainer = onPrimaryContainerDarkHighContrast,
secondary = secondaryDarkHighContrast,
onSecondary = onSecondaryDarkHighContrast,
secondaryContainer = secondaryContainerDarkHighContrast,
onSecondaryContainer = onSecondaryContainerDarkHighContrast,
tertiary = tertiaryDarkHighContrast,
onTertiary = onTertiaryDarkHighContrast,
tertiaryContainer = tertiaryContainerDarkHighContrast,
onTertiaryContainer = onTertiaryContainerDarkHighContrast,
error = errorDarkHighContrast,
onError = onErrorDarkHighContrast,
errorContainer = errorContainerDarkHighContrast,
onErrorContainer = onErrorContainerDarkHighContrast,
background = backgroundDarkHighContrast,
onBackground = onBackgroundDarkHighContrast,
surface = surfaceDarkHighContrast,
onSurface = onSurfaceDarkHighContrast,
surfaceVariant = surfaceVariantDarkHighContrast,
onSurfaceVariant = onSurfaceVariantDarkHighContrast,
outline = outlineDarkHighContrast,
outlineVariant = outlineVariantDarkHighContrast,
scrim = scrimDarkHighContrast,
inverseSurface = inverseSurfaceDarkHighContrast,
inverseOnSurface = inverseOnSurfaceDarkHighContrast,
inversePrimary = inversePrimaryDarkHighContrast,
surfaceDim = surfaceDimDarkHighContrast,
surfaceBright = surfaceBrightDarkHighContrast,
surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
surfaceContainerLow = surfaceContainerLowDarkHighContrast,
surfaceContainer = surfaceContainerDarkHighContrast,
surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
)
@Immutable
data class ColorFamily(
val color: Color,
val onColor: Color,
val colorContainer: Color,
val onColorContainer: Color
)
val unspecified_scheme = ColorFamily(
Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable() () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> darkScheme
else -> lightScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
content = content
)
}

View File

@@ -0,0 +1,9 @@
package com.sffteam.voidclient.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
val AppTypography = Typography()