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

18
.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
/.kotlin
/app/build
/app/release

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

101
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,101 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
kotlin("plugin.serialization") version "1.9.23"
}
android {
splits {
abi {
isEnable = true
reset()
include("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
isUniversalApk = true
}
}
namespace = "com.sffteam.voidclient"
compileSdk {
version = release(36)
}
defaultConfig {
applicationId = "com.sffteam.voidclient"
minSdk = 28
targetSdk = 36
versionCode = 1
versionName = "a2.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
// isShrinkResources = true
// isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
viewBinding = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.compiler)
implementation(libs.androidx.datastore.core)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.material3)
implementation(libs.androidx.ui)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.compose.adaptive)
implementation(libs.androidx.compose.material3.window.size.class1)
implementation(libs.androidx.compose.ui.text)
implementation(libs.androidx.compose.animation)
implementation(libs.androidx.compose.animation.core)
implementation(libs.androidx.compose.foundation.layout)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.androidx.foundation)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.media3.exoplayer)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.datastore.preferences)
implementation(libs.guava)
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
implementation("org.lz4:lz4-java:1.8.0")
implementation(libs.kotlinx.datetime)
implementation("io.ktor:ktor-client-cio:3.3.3")
implementation("io.ktor:ktor-client-core:3.3.3")
implementation("io.ktor:ktor-network:3.3.3")
implementation("io.ktor:ktor-network-tls:3.3.3")
implementation("org.msgpack:jackson-dataformat-msgpack:0.9.0")
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0")
implementation(libs.autolinktext)
implementation("io.github.g00fy2.quickie:quickie-bundled:1.11.0")
}

21
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,24 @@
package com.sffteam.openmax
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.sffteam.openmax", appContext.packageName)
}
}

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="Void Client"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.DayNight"
tools:targetApi="31">
<activity
android:name=".ProfileViewActivity"
android:exported="false" />
<activity
android:name=".ImageViewerActivity"
android:exported="false" />
<activity
android:name=".ChatEditActivity"
android:exported="false" />
<activity
android:name=".ChatViewActivity"
android:exported="false" />
<activity
android:name=".preferences.ChatSettingsActivity"
android:exported="false" />
<activity
android:name=".preferences.AboutActivity"
android:exported="false" />
<activity
android:name=".preferences.DevicesActivity"
android:exported="false" />
<activity
android:name=".PasswordCheckActivity"
android:exported="false" />
<activity
android:name=".preferences.SecurityActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.DayNight" />
<activity
android:name=".preferences.ProfileSettingsActivity"
android:exported="false" />
<activity
android:name=".preferences.SettingsActivity"
android:exported="false" />
<activity
android:name=".RegisterActivity"
android:exported="false" />
<activity
android:name=".ChatActivity"
android:exported="false"
android:label="@string/title_activity_chat"
android:theme="@style/Theme.AppCompat.DayNight" />
<activity
android:name=".ChatListActivity"
android:exported="false"
android:label="@string/title_activity_chat_list"
android:theme="@style/Theme.AppCompat.DayNight" />
<activity
android:name=".CodeActivity"
android:exported="false"
android:label="@string/title_activity_code"
android:theme="@style/Theme.OpenMax" />
<activity
android:name=".MainActivity"
android:exported="true"
android:label="Void Client"
android:theme="@style/Theme.OpenMax">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

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()

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".preferences.AboutActivity">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ChatEditActivity">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ChatViewActivity">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ImageViewerActivity">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".PasswordCheckActivity">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ProfileViewActivity">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -0,0 +1 @@
<resources></resources>

View File

@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.AppCompat.DayNight" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>

View File

@@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.AppCompat.DayNight" parent="Base.Theme.AppCompat.DayNight">
<!-- Transparent system bars for edge-to-edge. -->
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">?attr/isLightTheme</item>
</style>
</resources>

View File

@@ -0,0 +1 @@
<resources></resources>

View File

@@ -0,0 +1 @@
<resources></resources>

View File

@@ -0,0 +1,4 @@
<resources>
<!-- Reply Preference -->
</resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -0,0 +1 @@
<resources></resources>

View File

@@ -0,0 +1,7 @@
<resources>
<string name="title_activity_code">CodeActivity</string>
<string name="title_activity_chat_list">ChatListActivity</string>
<string name="title_activity_chat">ChatActivity</string>
<!-- Strings used for fragments for navigation -->
</resources>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.OpenMax" parent="android:Theme.Material.Light.NoActionBar" />
<style name="Theme.AppCompat.DayNight" parent="android:Theme.Material.Light.NoActionBar" />
<!-- Base application theme. -->
<style name="Base.Theme.AppCompat.DayNight" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,17 @@
package com.sffteam.openmax
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

6
build.gradle.kts Normal file
View File

@@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}

23
gradle.properties Normal file
View File

@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

82
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,82 @@
[versions]
agp = "8.13.0"
appcompat = "1.7.1"
coilCompose = "3.3.0"
datastorePreferencesCoreVersion = "1.1.7"
guava = "33.5.0-android"
kotlin = "2.2.21"
coreKtx = "1.17.0"
kotlinxDatetime = "0.7.1"
lifecycleRuntimeKtx = "2.9.4"
activityCompose = "1.11.0"
composeBom = "2025.10.01"
lifecycleViewmodelCompose = "2.10.0"
activity = "1.11.0"
kotlinxSerializationJson = "1.9.0"
material = "1.13.0"
compiler = "3.2.0-alpha11"
datastoreCoreVersion = "1.1.7"
roomKtx = "2.8.4"
foundation = "1.9.5"
textflowMaterial3 = "1.2.1"
material3 = "1.4.0"
ui = "1.9.5"
adaptive = "1.2.0"
material3WindowSizeClass = "1.4.0"
uiText = "1.10.0"
animation = "1.10.0"
animationCore = "1.10.0"
foundationLayout = "1.10.0"
navigationFragmentKtx = "2.6.0"
navigationUiKtx = "2.6.0"
foundationVersion = "1.10.0"
constraintlayout = "2.2.1"
media3Exoplayer = "1.9.2"
[libraries]
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferencesCoreVersion" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilCompose" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-compiler = { group = "androidx.databinding", name = "compiler", version.ref = "compiler" }
androidx-datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastoreCoreVersion" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomKtx" }
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }
textflow-material3 = { module = "io.github.oleksandrbalan:textflow-material3", version.ref = "textflowMaterial3" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" }
androidx-compose-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "adaptive" }
androidx-compose-material3-window-size-class1 = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "material3WindowSizeClass" }
autolinktext = { module = "sh.calvin.autolinktext:autolinktext", version = "2.0.2" }
androidx-compose-ui-text = { group = "androidx.compose.ui", name = "ui-text", version.ref = "uiText" }
androidx-compose-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "animation" }
androidx-compose-animation-core = { group = "androidx.compose.animation", name = "animation-core", version.ref = "animationCore" }
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" }
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundationVersion" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3Exoplayer" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,8 @@
#Sun Nov 02 14:28:35 EET 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Executable file
View File

@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

24
settings.gradle.kts Normal file
View File

@@ -0,0 +1,24 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Void Client"
include(":app")