Compare commits

..

No commits in common. "dev" and "master" have entirely different histories.
dev ... master

18 changed files with 100 additions and 639 deletions

View File

@ -1,3 +1,7 @@
<component name="CopyrightManager">
<settings default="My" />
<settings default="My">
<module2copyright>
<element module="All" copyright="My" />
</module2copyright>
</settings>
</component>

View File

@ -4,14 +4,6 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-02-22T11:46:39.159466074Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=22163a3c" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>

2
.idea/kotlinc.xml generated
View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.0.21" />
<option name="version" value="2.0.0" />
</component>
</project>

View File

@ -1,14 +1,13 @@
/*
* Created by sweetbread
* Created by sweetbread on 21.02.2025, 12:01
* Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 16:46
* Last modified 21.02.2025, 12:01
*/
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp")
}
android {
@ -35,11 +34,11 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "17"
jvmTarget = "1.8"
}
buildFeatures {
compose = true
@ -65,26 +64,12 @@ dependencies {
// Ktor - web client
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.logging)
implementation(libs.ktor.client.cio)
// Coil - image loader
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
// Room - database
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
// Navigation Compose
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.navigation.fragment)
implementation(libs.androidx.navigation.ui)
implementation(libs.androidx.navigation.dynamic.features.fragment)
androidTestImplementation(libs.androidx.navigation.testing)
// Others
implementation(libs.splitties.base) // Syntax sugar
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Created by sweetbread on 22.02.2025, 15:45
~ Created by sweetbread on 21.02.2025, 12:08
~ Copyright (c) 2025. All rights reserved.
~ Last modified 22.02.2025, 14:00
~ Last modified 21.02.2025, 12:07
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
@ -22,7 +22,13 @@
tools:targetApi="31">
<activity
android:name=".ui.activity.Login"
android:exported="false"
android:label="@string/title_activity_login"
android:theme="@style/Theme.PixelDragon" />
<activity
android:name=".ui.activity.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.PixelDragon">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -30,11 +36,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.activity.MainActivity"
android:exported="false"
android:theme="@style/Theme.PixelDragon" />
</application>
</manifest>

View File

@ -1,58 +1,43 @@
/*
* Created by sweetbread
* Created by sweetbread on 21.02.2025, 12:01
* Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 15:32
* Last modified 21.02.2025, 12:01
*/
package ru.risdeveau.pixeldragon
import android.content.Context
import android.util.Log
import androidx.room.Room
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.engine.cio.endpoint
import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import ru.risdeveau.pixeldragon.api.getMe
import ru.risdeveau.pixeldragon.db.AppDatabase
import splitties.init.appCtx
val client = HttpClient {
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Log.i("Ktor", message)
}
val client = HttpClient(CIO) {
engine {
endpoint {
maxConnectionsPerRoute = 100
pipelineMaxSize = 20
keepAliveTime = 30000
connectTimeout = 15000
connectAttempts = 5
}
level = LogLevel.ALL
}
install(HttpCache)
}
val accountData = appCtx.getSharedPreferences("settings", Context.MODE_PRIVATE)
lateinit var homeserver: String
lateinit var baseUrl: String
lateinit var urlBase: String
lateinit var token: String
suspend fun initCheck(): Boolean {
Log.d("initCheck", "checking...")
if (!accountData.contains("token")) return false
if (!accountData.contains("homeserver")) return false
token = accountData.getString("token", "").toString()
homeserver = accountData.getString("homeserver", "").toString()
baseUrl = "$homeserver/_matrix/client/v3"
Log.d("initCheck", "homeserver: $homeserver")
urlBase = "https://${accountData.getString("homeserver", "")}/_matrix/client/v3"
return getMe() != null
}
val db = Room.databaseBuilder(
appCtx,
AppDatabase::class.java, "database"
).build()
}

View File

@ -1,56 +0,0 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 20:21
*/
package ru.risdeveau.pixeldragon.api
import io.ktor.client.request.bearerAuth
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.statement.bodyAsText
import org.json.JSONObject
import ru.risdeveau.pixeldragon.baseUrl
import ru.risdeveau.pixeldragon.client
import ru.risdeveau.pixeldragon.token
class Event (
val id: String,
val rid: String,
val sender: String,
val type: String,
val content: JSONObject
) {
constructor(json: JSONObject) : this(
json.getString("event_id"),
json.getString("room_id"),
json.getString("sender"),
json.getString("type"),
json.getJSONObject("content")
)
}
data class EventsAround (
val base: Event,
val before: List<Event>,
val after: List<Event>
)
suspend fun getEventsAround(room: String, event: String): EventsAround {
val r = client.get("$baseUrl/rooms/$room/context/$event") {
bearerAuth(token)
parameter("limit", "50")
}
val json = JSONObject(r.bodyAsText())
return EventsAround(
Event(json.getJSONObject("event")),
if (json.has("events_before")) json.getJSONArray("events_before").let {
List<Event>(it.length()) { i -> Event(it.getJSONObject(i))}.reversed()
} else listOf(),
if (json.has("events_after")) json.getJSONArray("events_after").let {
List<Event>(it.length()) { i -> Event(it.getJSONObject(i))}
} else listOf()
)
}

View File

@ -1,63 +0,0 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 18:28
*/
package ru.risdeveau.pixeldragon.api
import io.ktor.client.request.bearerAuth
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import org.json.JSONObject
import ru.risdeveau.pixeldragon.baseUrl
import ru.risdeveau.pixeldragon.client
import ru.risdeveau.pixeldragon.db
import ru.risdeveau.pixeldragon.db.Room
import ru.risdeveau.pixeldragon.token
//fun getRooms(): List<Room> {
// return db.roomDoa().getAllJoined()
//}
//
//fun updateRooms(): List<Room> {
//
//}
suspend fun getRooms(): List<String> {
val r = client.get("$baseUrl/joined_rooms")
{ bearerAuth(token) }
val rooms = JSONObject(r.bodyAsText()).getJSONArray("joined_rooms")
return List<String>(
rooms.length()
) { i -> rooms.getString(i) }
}
suspend fun getRoom(rid: String): Room {
var room = db.roomDoa().getById(rid)
if (room == null) {
val name = getState(rid, "m.room.name", "name")
val type = getState(rid, "m.room.create", "type") ?: "m.room"
val creator = getState(rid, "m.room.create", "creator")
val avatar = getState(rid, "m.room.avatar", "url")
room = Room(rid, name, type, creator, null, avatar, null, true)
db.roomDoa().insert(room)
}
return room
}
private suspend fun getState(rid: String, state: String, key: String): String? {
val r = client.get("$baseUrl/rooms/$rid/state/$state") { bearerAuth(token) }
if (r.status != HttpStatusCode.OK) return null
val json = JSONObject(r.bodyAsText())
if (!json.has(key)) return null
return json.getString(key)
}
suspend fun getAccountData(user: String, room: String, state: String): JSONObject? {
val r = client.get("$baseUrl/user/$user/rooms/$room/account_data/$state") { bearerAuth(token) }
if (r.status != HttpStatusCode.OK) return null
return JSONObject(r.bodyAsText())
}

View File

@ -1,7 +1,7 @@
/*
* Created by sweetbread
* Created by sweetbread on 21.02.2025, 12:01
* Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 15:40
* Last modified 21.02.2025, 12:01
*/
package ru.risdeveau.pixeldragon.api
@ -11,26 +11,13 @@ import io.ktor.client.statement.bodyAsText
import org.json.JSONException
import org.json.JSONObject
import ru.risdeveau.pixeldragon.client
import ru.risdeveau.pixeldragon.homeserver
suspend fun getHomeserver(url: String): String? {
suspend fun isMatrixServer(url: String): Boolean {
val r = try { client.get("https://$url/.well-known/matrix/client") }
catch (_: Exception) { return null }
catch (_: Exception) { return false }
val json = try { JSONObject(r.bodyAsText()) }
catch (_: JSONException) { return null }
try { JSONObject(r.bodyAsText()) }
catch (_: JSONException) { return false }
if (!json.has("m.homeserver")) return null
var homeserver = json.getJSONObject("m.homeserver").getString("base_url")
if (homeserver.endsWith("/")) homeserver = homeserver.dropLast(1)
return homeserver
}
fun mxcToUrl(mxc: String): String {
val pattern = Regex("mxc://([-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})/([a-zA-Z]+)")
val match = pattern.find(mxc)
return "$homeserver/_matrix/client/v1/media/download/${match?.groupValues[1]}/${match?.groupValues[2]}"
return true
}

View File

@ -1,7 +1,7 @@
/*
* Created by sweetbread
* Created by sweetbread on 21.02.2025, 12:09
* Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 15:43
* Last modified 21.02.2025, 12:01
*/
package ru.risdeveau.pixeldragon.api
@ -18,10 +18,10 @@ import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import org.json.JSONObject
import ru.risdeveau.pixeldragon.accountData
import ru.risdeveau.pixeldragon.baseUrl
import ru.risdeveau.pixeldragon.client
import ru.risdeveau.pixeldragon.initCheck
import ru.risdeveau.pixeldragon.token
import ru.risdeveau.pixeldragon.urlBase
import splitties.init.appCtx
data class Me (val userId: String, val deviceId: String)
@ -30,7 +30,7 @@ data class Me (val userId: String, val deviceId: String)
* This func is to validate the token
*/
suspend fun getMe(): Me? {
val r = client.get("$baseUrl/account/whoami") { bearerAuth(token) }
val r = client.get("$urlBase/account/whoami") { bearerAuth(token) }
if (r.status != HttpStatusCode.OK) {
Log.e("getMe", r.bodyAsText())
return null
@ -41,10 +41,8 @@ suspend fun getMe(): Me? {
}
@SuppressLint("ApplySharedPref")
suspend fun login(server: String, login: String, pass: String): Boolean {
val homeserver = getHomeserver(server)!!
val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0)
suspend fun login(homeserver: String, login: String, pass: String): Boolean {
val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0);
val pattern = """
{
@ -60,7 +58,7 @@ suspend fun login(server: String, login: String, pass: String): Boolean {
json.put("password", pass)
val r = try {
client.post("$homeserver/_matrix/client/v3/login") {
client.post("https://$homeserver/_matrix/client/v3/login") {
setBody(json.toString())
contentType(ContentType.Application.Json)
}
@ -77,7 +75,7 @@ suspend fun login(server: String, login: String, pass: String): Boolean {
val res = JSONObject(r.bodyAsText())
val editor = accountData.edit()
editor.putString("token", res.getString("access_token"))
editor.putString("homeserver", homeserver)
editor.putString("homeserver", res.getString("home_server"))
editor.commit()
return initCheck()

View File

@ -1,81 +0,0 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
* Last modified 22.02.2025, 19:49
*/
package ru.risdeveau.pixeldragon.db
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Delete
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.Junction
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Relation
import androidx.room.RoomDatabase
@Entity
data class Room(
@PrimaryKey val roomId: String,
val name: String?,
val type: String,
val creatorId: String?,
val createTime: Long?,
val avatarUrl: String?,
val members: Int?,
val joined: Boolean
)
@Dao
interface RoomDao {
// @Query("SELECT * FROM room")
// fun getAll(): List<User>
// @Query("SELECT * FROM user WHERE uid IN (:userIds)")
// fun loadAllByIds(userIds: IntArray): List<User>
// @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
// "last_name LIKE :last LIMIT 1")
// fun findByName(first: String, last: String): User
@Query("SELECT * FROM room WHERE roomId LIKE :rid LIMIT 1")
fun getById(rid: String): Room?
// @Transaction
// @Query("SELECT * FROM room WHERE ")
// fun getSpace(rid: String): Space
@Query("SELECT * FROM room WHERE joined = 1 AND roomId NOT IN (SELECT roomId FROM SpaceToRoom)")
fun getAllJoined(): List<Room>
@Insert
fun insert(vararg rooms: Room)
@Delete
fun delete(room: Room)
}
@Entity(primaryKeys = ["spaceId", "roomId"])
data class SpaceToRoom(
val spaceId: String,
val roomId: String
)
data class Space(
@Embedded val space: Room,
@Relation(
parentColumn = "spaceId",
entityColumn = "roomId",
associateBy = Junction(SpaceToRoom::class)
)
val children: List<Room>
)
@Database(entities = [Room::class, SpaceToRoom::class, User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun roomDoa(): RoomDao
}

View File

@ -1,18 +0,0 @@
/*
* Created by sweetbread on 22.02.2025, 15:45
* Copyright (c) 2025. All rights reserved.
* Last modified 21.02.2025, 13:38
*/
package ru.risdeveau.pixeldragon.db
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class User(
@PrimaryKey val uid: String,
@ColumnInfo(name = "name") val name: String?,
@ColumnInfo(name = "avatar") val avatar: String?
)

View File

@ -1,7 +1,7 @@
/*
* Created by sweetbread
* Created by sweetbread on 21.02.2025, 12:08
* Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 16:08
* Last modified 21.02.2025, 12:08
*/
package ru.risdeveau.pixeldragon.ui.activity
@ -35,28 +35,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import ru.risdeveau.pixeldragon.api.getHomeserver
import ru.risdeveau.pixeldragon.api.isMatrixServer
import ru.risdeveau.pixeldragon.api.login
import ru.risdeveau.pixeldragon.initCheck
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
import splitties.activities.start
class Login : ComponentActivity() {
@OptIn(DelicateCoroutinesApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GlobalScope.launch {
if (initCheck()) {
start<MainActivity>()
finish()
}
}
enableEdgeToEdge()
setContent {
PixelDragonTheme {
@ -69,10 +56,7 @@ class Login : ComponentActivity() {
SnackbarHost(hostState = snackbarHostState)
}
) { innerPadding ->
Box(
Modifier
.fillMaxSize()
.padding(innerPadding)) {
Box(Modifier.fillMaxSize().padding(innerPadding)) {
LoginField(
Modifier.align(Alignment.Center),
{ start<MainActivity>() },
@ -105,7 +89,7 @@ fun LoginField(modifier: Modifier = Modifier, ok: () -> Unit, err: () -> Unit) {
var login by remember { mutableStateOf("") }
var pass by remember { mutableStateOf("") }
var hmsValid: Boolean by remember { mutableStateOf(false) }
var hmsValid by remember { mutableStateOf(false) }
OutlinedTextField(
modifier = Modifier.padding(4.dp),
@ -135,17 +119,18 @@ fun LoginField(modifier: Modifier = Modifier, ok: () -> Unit, err: () -> Unit) {
Button(
enabled = hmsValid && !login.isEmpty() && !pass.isEmpty(),
onClick = {
scope.launch {
if (login(homeserver, login, pass)) ok()
else err()
}
var loginSuccess = false
scope.launch { loginSuccess = login(homeserver, login, pass) }
if (loginSuccess) ok()
else err()
}
) {
Text("Login")
}
LaunchedEffect(homeserver) {
scope.launch { hmsValid = (getHomeserver(homeserver) != null) }
scope.launch { hmsValid = isMatrixServer(homeserver) }
}
}
}

View File

@ -1,7 +1,7 @@
/*
* Created by sweetbread
* Created by sweetbread on 21.02.2025, 12:08
* Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 20:22
* Last modified 21.02.2025, 12:07
*/
package ru.risdeveau.pixeldragon.ui.activity
@ -18,19 +18,23 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import ru.risdeveau.pixeldragon.ui.layout.Room
import ru.risdeveau.pixeldragon.ui.layout.RoomList
import androidx.compose.ui.tooling.preview.Preview
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import ru.risdeveau.pixeldragon.initCheck
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
import splitties.activities.start
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, DelicateCoroutinesApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
GlobalScope.launch {
if (!initCheck()) start<Login>()
}
super.onCreate(savedInstanceState)
enableEdgeToEdge()
@ -50,24 +54,28 @@ class MainActivity : ComponentActivity() {
)
},
) { innerPadding ->
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "rooms") {
composable("rooms") { RoomList(Modifier.padding(innerPadding), navController) }
composable(
"room/{rid}",
arguments = listOf(navArgument("rid") { type = NavType.StringType })
) { navBackStackEntry ->
Room(Modifier.padding(innerPadding).fillMaxSize(), navBackStackEntry.arguments!!.getString("rid")!!)
}
composable(
"space/{rid}",
arguments = listOf(navArgument("rid") { type = NavType.StringType })
) { navBackStackEntry ->
Text(modifier = Modifier.padding(innerPadding), text = "Not implemented") }
}
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
PixelDragonTheme {
Greeting("Android")
}
}

View File

@ -1,90 +0,0 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 21:30
*/
package ru.risdeveau.pixeldragon.ui.layout
import androidx.compose.foundation.background
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.text.font.FontWeight
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ru.risdeveau.pixeldragon.api.Event
import ru.risdeveau.pixeldragon.api.getAccountData
import ru.risdeveau.pixeldragon.api.getEventsAround
import ru.risdeveau.pixeldragon.api.getMe
@Composable
fun Room(modifier: Modifier = Modifier, rid: String) {
var eventsId by remember { mutableStateOf(listOf<Event>()) }
val coroutineScope = rememberCoroutineScope()
val listState = rememberLazyListState()
LaunchedEffect(true) {
coroutineScope.launch {
withContext(Dispatchers.IO) {
val readMark = getAccountData(getMe()!!.userId, rid, "m.fully_read")
val eventsAround = getEventsAround(rid, readMark!!.getString("event_id")) //FIXME: Null check
eventsId = eventsAround.let {
it.before + listOf(it.base) + it.after
}
}
}
}
LazyColumn(modifier = modifier, state = listState, reverseLayout = true) {
items(eventsId.reversed()) { event ->
EventItem(event)
}
item {
if (eventsId.isEmpty()) {
Text("Empty room")
}
}
}
}
@Composable
fun EventItem(event: Event) {
Box (Modifier.fillMaxWidth()) {
when (event.type) {
"m.room.message" -> Column(
Modifier
.align(Alignment.CenterStart)
.padding(4.dp)
.background(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = RoundedCornerShape(16.dp)
)
.padding(4.dp)
) {
Text(event.sender, maxLines = 1, fontWeight = FontWeight.Bold)
Text(event.content.getString("body"))
}
else -> Text(event.type, Modifier.padding(4.dp).background(MaterialTheme.colorScheme.errorContainer).padding(4.dp))
}
}
}

View File

@ -1,161 +0,0 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 20:22
*/
package ru.risdeveau.pixeldragon.ui.layout
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.padding
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.material3.CircularProgressIndicator
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import coil3.compose.SubcomposeAsyncImage
import coil3.network.NetworkHeaders
import coil3.network.httpHeaders
import coil3.request.ImageRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ru.risdeveau.pixeldragon.api.getRoom
import ru.risdeveau.pixeldragon.api.getRooms
import ru.risdeveau.pixeldragon.api.mxcToUrl
import ru.risdeveau.pixeldragon.db.Room
import ru.risdeveau.pixeldragon.token
import splitties.init.appCtx
@Composable
fun RoomList(modifier: Modifier = Modifier, navController: NavController) {
var list by remember { mutableStateOf(listOf<String>()) }
val coroutineScope = rememberCoroutineScope()
val listState = rememberLazyListState()
// if (itemState.scrollToTop) {
// LaunchedEffect(coroutineScope) {
// Log.e("TAG", "TopCoinsScreen: scrollToTop" )
// listState.scrollToItem(0)
// }
// }
LaunchedEffect(true) {
coroutineScope.launch {
withContext(Dispatchers.IO) {
list = getRooms()
}
}
}
LazyColumn(modifier = modifier, state = listState) {
items(list) { rid ->
RoomItem(rid = rid, navController = navController )
}
item {
if (list.isEmpty()) {
Text("You have no rooms")
}
}
}
}
@Composable
fun RoomItem(modifier: Modifier = Modifier, rid: String, navController: NavController) {
var room by remember { mutableStateOf<Room?>(null) }
val scope = rememberCoroutineScope()
LaunchedEffect(true) {
scope.launch {
withContext(Dispatchers.IO) {
room = getRoom(rid)
}
}
}
if (room != null) {
Row(
modifier
.padding(8.dp)
.height((52 + 8 * 2).dp)
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background, RoundedCornerShape(16.dp))
.padding(8.dp)
.clickable {
if (room!!.type == "m.space")
navController.navigate("space/$rid")
else
navController.navigate("room/$rid")
}
) {
SubcomposeAsyncImage(
modifier = Modifier
.padding(end = 4.dp)
.height(52.dp)
.width(52.dp)
.let {
if (room!!.type == "m.space")
it.clip(RoundedCornerShape(12.dp))
else
it.clip(CircleShape)
},
model = ImageRequest.Builder(appCtx)
.data(mxcToUrl(room!!.avatarUrl ?: ""))
.httpHeaders(
NetworkHeaders.Builder()
.set("Authorization", "Bearer $token")
.set("Cache-Control", "max-age=86400")
.build()
)
.build(),
contentDescription = room!!.roomId,
contentScale = ContentScale.Crop,
loading = {
CircularProgressIndicator()
}
)
Column {
Text(
room!!.name ?: "Unnamed",
maxLines = 1,
color = MaterialTheme.colorScheme.primary,
fontSize = MaterialTheme.typography.titleLarge.fontSize
)
}
}
} else {
Row(
modifier
.padding(8.dp)
.height(80.dp)
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background, RoundedCornerShape(16.dp))
.padding(8.dp)
) {
LinearProgressIndicator(Modifier.fillMaxWidth())
}
}
}

View File

@ -1,7 +1,7 @@
/*
* Created by sweetbread on 22.02.2025, 15:45
* Created by sweetbread on 21.02.2025, 12:00
* Copyright (c) 2025. All rights reserved.
* Last modified 21.02.2025, 12:21
* Last modified 21.02.2025, 12:00
*/
// Top-level build file where you can add configuration options common to all sub-projects/modules.
@ -9,5 +9,4 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
}

View File

@ -1,31 +1,19 @@
[versions]
agp = "8.7.3"
coil = "3.1.0"
kotlin = "2.0.21"
kotlin = "2.0.0"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
kotlinxSerializationJson = "1.7.3"
ktor = "3.1.0"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.10.0"
composeBom = "2025.02.00"
navigationCompose = "2.8.8"
room = "2.6.1"
splittiesFunPackAndroidBase = "3.0.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "room" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
androidx-navigation-dynamic-features-fragment = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigationCompose" }
androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "navigationCompose" }
androidx-navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navigationCompose" }
androidx-navigation-ui = { module = "androidx.navigation:navigation-ui", version.ref = "navigationCompose" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
@ -41,10 +29,8 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
splitties-base = { module = "com.louiscad.splitties:splitties-fun-pack-android-base", version.ref = "splittiesFunPackAndroidBase" }
[plugins]