init
18
.gitignore
vendored
Normal 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
@@ -0,0 +1 @@
|
||||
/build
|
||||
101
app/build.gradle.kts
Normal 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
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
82
app/src/main/AndroidManifest.xml
Normal 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>
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
124
app/src/main/java/com/sffteam/voidclient/AccountManager.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2434
app/src/main/java/com/sffteam/voidclient/ChatActivity.kt
Normal file
385
app/src/main/java/com/sffteam/voidclient/ChatEditActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
750
app/src/main/java/com/sffteam/voidclient/ChatListActivity.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
641
app/src/main/java/com/sffteam/voidclient/ChatManager.kt
Normal 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
|
||||
}
|
||||
}
|
||||
470
app/src/main/java/com/sffteam/voidclient/ChatViewActivity.kt
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
170
app/src/main/java/com/sffteam/voidclient/CodeActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
247
app/src/main/java/com/sffteam/voidclient/ImageViewerActivity.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
256
app/src/main/java/com/sffteam/voidclient/MainActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("Войти")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
319
app/src/main/java/com/sffteam/voidclient/ProfileViewActivity.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
134
app/src/main/java/com/sffteam/voidclient/RegisterActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
490
app/src/main/java/com/sffteam/voidclient/SocketManager.kt
Normal 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!")
|
||||
}
|
||||
}
|
||||
}
|
||||
204
app/src/main/java/com/sffteam/voidclient/UserManager.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
app/src/main/java/com/sffteam/voidclient/Utils.kt
Normal 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]
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
225
app/src/main/java/com/sffteam/voidclient/ui/theme/Color.kt
Normal 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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
280
app/src/main/java/com/sffteam/voidclient/ui/theme/Theme.kt
Normal 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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
74
app/src/main/res/drawable/ic_launcher_background.xml
Normal 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>
|
||||
10
app/src/main/res/layout/activity_about.xml
Normal 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>
|
||||
10
app/src/main/res/layout/activity_chat_edit.xml
Normal 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>
|
||||
10
app/src/main/res/layout/activity_chat_view.xml
Normal 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>
|
||||
10
app/src/main/res/layout/activity_image_viewer.xml
Normal 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>
|
||||
10
app/src/main/res/layout/activity_password_check2.xml
Normal 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>
|
||||
10
app/src/main/res/layout/activity_profile_view.xml
Normal 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>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal 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>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal 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>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 832 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
1
app/src/main/res/values-land/dimens.xml
Normal file
@@ -0,0 +1 @@
|
||||
<resources></resources>
|
||||
7
app/src/main/res/values-night/themes.xml
Normal 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>
|
||||
9
app/src/main/res/values-v23/themes.xml
Normal 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>
|
||||
1
app/src/main/res/values-w1240dp/dimens.xml
Normal file
@@ -0,0 +1 @@
|
||||
<resources></resources>
|
||||
1
app/src/main/res/values-w600dp/dimens.xml
Normal file
@@ -0,0 +1 @@
|
||||
<resources></resources>
|
||||
4
app/src/main/res/values/arrays.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<!-- Reply Preference -->
|
||||
|
||||
</resources>
|
||||
2
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
1
app/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1 @@
|
||||
<resources></resources>
|
||||
7
app/src/main/res/values/strings.xml
Normal 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>
|
||||
12
app/src/main/res/values/themes.xml
Normal 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>
|
||||
13
app/src/main/res/xml/backup_rules.xml
Normal 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>
|
||||
19
app/src/main/res/xml/data_extraction_rules.xml
Normal 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>
|
||||
17
app/src/test/java/com/sffteam/voidclient/ExampleUnitTest.kt
Normal 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
@@ -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
@@ -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
@@ -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
8
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
@@ -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
@@ -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
@@ -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")
|
||||
|
||||