Compare commits

...
11 Commits
15 changed files with 443 additions and 206 deletions

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "ru.risdeveau.pixeldragon" applicationId = "ru.risdeveau.pixeldragon"
minSdk = 24 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
@@ -1,22 +1,18 @@
/* /*
* Created by sweetbread * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 15:32
*/ */
package ru.risdeveau.pixeldragon package ru.risdeveau.pixeldragon
import android.content.Context
import android.util.Log import android.util.Log
import androidx.room.Room
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.plugins.cache.HttpCache import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging import io.ktor.client.plugins.logging.Logging
import ru.risdeveau.pixeldragon.api.getMe import ru.risdeveau.pixeldragon.api.getMe
import ru.risdeveau.pixeldragon.db.AppDatabase import splitties.preferences.Preferences
import splitties.init.appCtx
val client = HttpClient { val client = HttpClient {
install(Logging) { install(Logging) {
@@ -31,28 +27,24 @@ val client = HttpClient {
install(HttpCache) install(HttpCache)
} }
val accountData = appCtx.getSharedPreferences("settings", Context.MODE_PRIVATE)
lateinit var homeserver: String lateinit var homeserver: String
lateinit var baseUrl: String lateinit var baseUrl: String
lateinit var token: String lateinit var token: String
object AccountData : Preferences("settings") {
var token by stringPref("token", "")
var homeserver by stringPref("homeserver", "")
}
suspend fun initCheck(): Boolean { suspend fun initCheck(): Boolean {
Log.d("initCheck", "checking...") Log.d("initCheck", "checking...")
if (!accountData.contains("token")) return false token = AccountData.token
if (!accountData.contains("homeserver")) return false homeserver = AccountData.homeserver
if (token.isEmpty() or homeserver.isEmpty()) return false
token = accountData.getString("token", "").toString()
homeserver = accountData.getString("homeserver", "").toString()
baseUrl = "$homeserver/_matrix/client/v3" baseUrl = "$homeserver/_matrix/client/v3"
Log.d("initCheck", "homeserver: $homeserver")
return getMe() != null return getMe() != null
} }
val db = Room.databaseBuilder(
appCtx,
AppDatabase::class.java, "database"
).build()
@@ -1,7 +1,6 @@
/* /*
* Created by sweetbread * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 18:28
*/ */
package ru.risdeveau.pixeldragon.api package ru.risdeveau.pixeldragon.api
@@ -10,11 +9,13 @@ import io.ktor.client.request.bearerAuth
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.json.JSONObject import org.json.JSONObject
import ru.risdeveau.pixeldragon.baseUrl import ru.risdeveau.pixeldragon.baseUrl
import ru.risdeveau.pixeldragon.client import ru.risdeveau.pixeldragon.client
import ru.risdeveau.pixeldragon.db import ru.risdeveau.pixeldragon.repo.Room
import ru.risdeveau.pixeldragon.db.Room import ru.risdeveau.pixeldragon.repo.User
import ru.risdeveau.pixeldragon.token import ru.risdeveau.pixeldragon.token
//fun getRooms(): List<Room> { //fun getRooms(): List<Room> {
@@ -25,27 +26,60 @@ import ru.risdeveau.pixeldragon.token
// //
//} //}
suspend fun getRooms(): List<String> { suspend fun getJoinedRooms(): List<Room> {
val r = client.get("$baseUrl/joined_rooms")
{ bearerAuth(token) }
val rooms = JSONObject(r.bodyAsText()).getJSONArray("joined_rooms")
return List(
rooms.length()
) { i -> getRoom(rooms.getString(i), true) }
}
suspend fun isJoined(id: String): Boolean {
val r = client.get("$baseUrl/joined_rooms") val r = client.get("$baseUrl/joined_rooms")
{ bearerAuth(token) } { bearerAuth(token) }
val rooms = JSONObject(r.bodyAsText()).getJSONArray("joined_rooms") val rooms = JSONObject(r.bodyAsText()).getJSONArray("joined_rooms")
return List<String>( return List<String>(
rooms.length() rooms.length()
) { i -> rooms.getString(i) } ) { i -> rooms.getString(i) }.contains(id)
} }
suspend fun getRoom(rid: String): Room { suspend fun getRoom(rid: String, joined: Boolean? = null): Room {
var room = db.roomDoa().getById(rid) val direct = getAccountData(getMe()!!.userId, "m.direct")
if (room == null) { var directWith = ""
val name = getState(rid, "m.room.name", "name") direct?.let {
val type = getState(rid, "m.room.create", "type") ?: "m.room" for (user in direct.keys()) {
val creator = getState(rid, "m.room.create", "creator") val roomsWithUser = direct.getJSONArray(user)
val avatar = getState(rid, "m.room.avatar", "url") for (i in 0 until roomsWithUser.length()) {
room = Room(rid, name, type, creator, null, avatar, null, true) if (rid == roomsWithUser.getString(i)) {
db.roomDoa().insert(room) directWith = user
break
}
}
if (directWith.isNotEmpty()) break
}
} }
return room return coroutineScope {
val name = async { getState(rid, "m.room.name", "name") }
val type = async { getState(rid, "m.room.create", "type") ?: "m.room" }
val creator = async { getState(rid, "m.room.create", "creator") }
val avatar = async { getState(rid, "m.room.avatar", "url") }
val joined = async { joined ?: isJoined(rid) }
val direct = async { if (directWith.isNotEmpty()) User.getById(directWith) else null }
Room(
rid,
name.await(),
type.await(),
creator.await(),
null,
avatar.await(),
null,
joined.await(),
direct.await()
)
}
} }
private suspend fun getState(rid: String, state: String, key: String): String? { private suspend fun getState(rid: String, state: String, key: String): String? {
@@ -1,7 +1,6 @@
/* /*
* Created by sweetbread * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 15:40
*/ */
package ru.risdeveau.pixeldragon.api package ru.risdeveau.pixeldragon.api
@@ -28,9 +27,11 @@ suspend fun getHomeserver(url: String): String? {
return homeserver return homeserver
} }
fun mxcToUrl(mxc: String): String { 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 pattern = Regex("mxc://([-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})/([^#?]+)")
val match = pattern.find(mxc) val match = pattern.find(mxc)
if ((match?.groupValues[1] == null) or (match?.groupValues[2] == null)) return null
return "$homeserver/_matrix/client/v1/media/download/${match?.groupValues[1]}/${match?.groupValues[2]}" return "$homeserver/_matrix/client/v1/media/download/${match?.groupValues[1]}/${match?.groupValues[2]}"
} }
@@ -1,12 +1,10 @@
/* /*
* Created by sweetbread * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 15:43
*/ */
package ru.risdeveau.pixeldragon.api package ru.risdeveau.pixeldragon.api
import android.annotation.SuppressLint
import android.util.Log import android.util.Log
import io.ktor.client.request.bearerAuth import io.ktor.client.request.bearerAuth
import io.ktor.client.request.get import io.ktor.client.request.get
@@ -17,14 +15,17 @@ import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType import io.ktor.http.contentType
import org.json.JSONObject import org.json.JSONObject
import ru.risdeveau.pixeldragon.accountData import ru.risdeveau.pixeldragon.AccountData
import ru.risdeveau.pixeldragon.baseUrl import ru.risdeveau.pixeldragon.baseUrl
import ru.risdeveau.pixeldragon.client import ru.risdeveau.pixeldragon.client
import ru.risdeveau.pixeldragon.initCheck import ru.risdeveau.pixeldragon.initCheck
import ru.risdeveau.pixeldragon.token import ru.risdeveau.pixeldragon.token
import splitties.experimental.ExperimentalSplittiesApi
import splitties.init.appCtx import splitties.init.appCtx
import splitties.preferences.edit
data class Me (val userId: String, val deviceId: String) data class Me (val userId: String, val deviceId: String)
data class UserProfile (val displayName: String, val avatarUrl: String, val other: JSONObject)
/** /**
* This func is to validate the token * This func is to validate the token
@@ -40,9 +41,9 @@ suspend fun getMe(): Me? {
return Me(json.getString("user_id"), json.getString("device_id")) return Me(json.getString("user_id"), json.getString("device_id"))
} }
@SuppressLint("ApplySharedPref") @OptIn(ExperimentalSplittiesApi::class)
suspend fun login(server: String, login: String, pass: String): Boolean { suspend fun login(server: String, login: String, pass: String): Boolean {
val homeserver = getHomeserver(server)!! val hs = getHomeserver(server)!!
val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0) val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0)
@@ -60,7 +61,7 @@ suspend fun login(server: String, login: String, pass: String): Boolean {
json.put("password", pass) json.put("password", pass)
val r = try { val r = try {
client.post("$homeserver/_matrix/client/v3/login") { client.post("$hs/_matrix/client/v3/login") {
setBody(json.toString()) setBody(json.toString())
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
} }
@@ -75,10 +76,25 @@ suspend fun login(server: String, login: String, pass: String): Boolean {
} }
val res = JSONObject(r.bodyAsText()) val res = JSONObject(r.bodyAsText())
val editor = accountData.edit() AccountData.edit {
editor.putString("token", res.getString("access_token")) token = res.getString("access_token")
editor.putString("homeserver", homeserver) homeserver = hs
editor.commit() }
return initCheck() return initCheck()
} }
suspend fun getAccountData(user: String, state: String): JSONObject? {
val r = client.get("$baseUrl/user/$user/account_data/$state") { bearerAuth(token) }
if (r.status != HttpStatusCode.OK) return null
return JSONObject(r.bodyAsText())
}
suspend fun getUserProfile(userId: String): UserProfile? {
val r = client.get("$baseUrl/profile/$userId") { bearerAuth(token) }
if (r.status != HttpStatusCode.OK) return null
val json = JSONObject(r.bodyAsText())
val name = json.optString("displayname", ""); json.remove("displayname")
val avatar = json.optString("avatar_url", ""); json.remove("avatar_url")
return UserProfile(name, avatar, json)
}
+41
View File
@@ -0,0 +1,41 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
package ru.risdeveau.pixeldragon.db
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverter
import splitties.init.appCtx
import splitties.systemservices.connectivityManager
import java.time.Instant
@Database(entities = [RoomDB::class, SpaceToRoom::class, UserDB::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun roomDoa(): RoomDao
abstract fun userDoa(): UserDao
}
class Converters {
@TypeConverter
fun fromInstant(value: Instant?): String? {
return value?.toString()
}
@TypeConverter
fun toInstant(value: String?): Instant? {
return value?.let { Instant.parse(it) }
}
}
fun isConnected(): Boolean = connectivityManager.isDefaultNetworkActive
val cacheDb = Room.databaseBuilder(
appCtx,
AppDatabase::class.java, "cache"
)
.fallbackToDestructiveMigration()
.build()
@@ -1,62 +1,87 @@
/* /*
* Created by sweetbread * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2025. All rights reserved.
* Last modified 22.02.2025, 19:49
*/ */
package ru.risdeveau.pixeldragon.db package ru.risdeveau.pixeldragon.db
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Database
import androidx.room.Delete import androidx.room.Delete
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Junction import androidx.room.Junction
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.Query import androidx.room.Query
import androidx.room.Relation import androidx.room.Relation
import androidx.room.RoomDatabase import androidx.room.TypeConverters
import ru.risdeveau.pixeldragon.repo.Room
import ru.risdeveau.pixeldragon.repo.User
import java.time.Instant
@Entity @Entity(tableName = "room")
data class Room( @TypeConverters(Converters::class)
@PrimaryKey val roomId: String, data class RoomDB (
@PrimaryKey val id: String,
val updatedAt: Instant,
val name: String?, val name: String?,
val type: String, val type: String,
val creatorId: String?, val creatorId: String?,
val createTime: Long?, val createTime: Long?,
val avatarUrl: String?, val avatarUrl: String?,
val members: Int?, val members: Int?,
val joined: Boolean val joined: Boolean,
val direct: String?
) )
fun RoomDB.isExpired(cacheDuration: Long = 60 * 60): Boolean {
return Instant.now().minusSeconds(cacheDuration) > updatedAt
}
suspend fun RoomDB.toDomain(): Room = Room(
id = id,
name = name,
type = type,
creatorId = creatorId,
createTime = createTime,
avatarUrl = avatarUrl,
members = members,
joined = joined,
direct = direct?.let { User.getById(it) }
)
fun Room.toEntity(): RoomDB = RoomDB(
id = id,
updatedAt = Instant.now(),
name = name,
type = type,
creatorId = creatorId,
createTime = createTime,
avatarUrl = avatarUrl,
members = members,
joined = joined,
direct = direct?.id
)
@Dao @Dao
interface RoomDao { interface RoomDao {
// @Query("SELECT * FROM room") @Query("SELECT * FROM room WHERE id LIKE :id LIMIT 1")
// fun getAll(): List<User> fun getById(id: String): RoomDB?
// @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 // @Transaction
// @Query("SELECT * FROM room WHERE ") // @Query("SELECT * FROM room WHERE ")
// fun getSpace(rid: String): Space // fun getSpace(rid: String): Space
@Query("SELECT * FROM room WHERE joined = 1 AND roomId NOT IN (SELECT roomId FROM SpaceToRoom)") @Query("SELECT * FROM room WHERE joined = 1 AND id NOT IN (SELECT id FROM SpaceToRoom)")
fun getAllJoined(): List<Room> fun getAllJoined(): List<RoomDB>
@Insert @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg rooms: Room) fun insert(vararg rooms: RoomDB)
@Delete @Delete
fun delete(room: Room) fun delete(room: RoomDB)
} }
@Entity(primaryKeys = ["spaceId", "roomId"]) @Entity(primaryKeys = ["spaceId", "roomId"])
@@ -66,16 +91,11 @@ data class SpaceToRoom(
) )
data class Space( data class Space(
@Embedded val space: Room, @Embedded val space: RoomDB,
@Relation( @Relation(
parentColumn = "spaceId", parentColumn = "spaceId",
entityColumn = "roomId", entityColumn = "roomId",
associateBy = Junction(SpaceToRoom::class) associateBy = Junction(SpaceToRoom::class)
) )
val children: List<Room> val children: List<RoomDB>
) )
@Database(entities = [Room::class, SpaceToRoom::class, User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun roomDoa(): RoomDao
}
@@ -1,18 +1,60 @@
/* /*
* Created by sweetbread on 22.02.2025, 15:45 * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2025. All rights reserved.
* Last modified 21.02.2025, 13:38
*/ */
package ru.risdeveau.pixeldragon.db package ru.risdeveau.pixeldragon.db
import androidx.room.ColumnInfo import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.TypeConverters
import org.json.JSONObject
import ru.risdeveau.pixeldragon.repo.User
import java.time.Instant
@Entity @Entity(tableName = "user")
data class User( @TypeConverters(Converters::class)
@PrimaryKey val uid: String, data class UserDB (
@ColumnInfo(name = "name") val name: String?, @PrimaryKey val id: String,
@ColumnInfo(name = "avatar") val avatar: String? val updatedAt: Instant,
val name: String?,
val avatar: String?,
val attrs: String,
) )
fun UserDB.isExpired(cacheDuration: Long = 60 * 60): Boolean {
return Instant.now().minusSeconds(cacheDuration) > updatedAt
}
fun UserDB.toDomain(): User = User(
id = id,
name = name,
avatarUrl = avatar,
attrs = JSONObject(attrs),
)
fun User.toEntity(): UserDB = UserDB(
id = id,
updatedAt = Instant.now(),
name = name,
avatar = avatarUrl,
attrs = attrs.toString().trim(),
)
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE id LIKE :id LIMIT 1")
fun getById(id: String): UserDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg users: UserDB)
@Delete
fun delete(user: UserDB)
}
+62
View File
@@ -0,0 +1,62 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
package ru.risdeveau.pixeldragon.repo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ru.risdeveau.pixeldragon.api.getJoinedRooms
import ru.risdeveau.pixeldragon.api.getRoom
import ru.risdeveau.pixeldragon.db.cacheDb
import ru.risdeveau.pixeldragon.db.isConnected
import ru.risdeveau.pixeldragon.db.isExpired
import ru.risdeveau.pixeldragon.db.toDomain
import ru.risdeveau.pixeldragon.db.toEntity
class Room (
val id: String,
val name: String?,
val type: String,
val creatorId: String?,
val createTime: Long?,
val avatarUrl: String?,
val members: Int?,
val joined: Boolean,
val direct: User?,
) {
companion object {
suspend fun getById(id: String, cached: Boolean = true): Room {
val cachedRoom = cacheDb.roomDoa().getById(id)
if (!isConnected() and
(!cached or (cachedRoom == null) or (cachedRoom?.isExpired() == true))
) {
val room = getRoom(id)
CoroutineScope(Dispatchers.IO).launch {
val cacheRoom = room.toEntity()
cacheDb.roomDoa().insert(cacheRoom)
}
return room
}
return cachedRoom!!.toDomain()
}
suspend fun getJoined(cached: Boolean = true): List<Room> {
val cacheJoined = cacheDb.roomDoa().getAllJoined()
if (isConnected() and
(!cached or cacheJoined.isEmpty() or (cacheJoined.any { it.isExpired() }))
) {
val rooms = getJoinedRooms()
CoroutineScope(Dispatchers.IO).launch {
val roomsDb = List(rooms.size) { i -> rooms[i].toEntity() }
cacheDb.roomDoa().insert(*roomsDb.toTypedArray())
}
return rooms
}
return List(cacheJoined.size) { i -> cacheJoined[i].toDomain() }
}
}
}
+51
View File
@@ -0,0 +1,51 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
package ru.risdeveau.pixeldragon.repo
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONObject
import ru.risdeveau.pixeldragon.api.getUserProfile
import ru.risdeveau.pixeldragon.db.cacheDb
import ru.risdeveau.pixeldragon.db.isConnected
import ru.risdeveau.pixeldragon.db.isExpired
import ru.risdeveau.pixeldragon.db.toDomain
import ru.risdeveau.pixeldragon.db.toEntity
class User (
val id: String,
val name: String?,
val avatarUrl: String?,
val attrs: JSONObject
) {
companion object {
suspend fun getById(id: String, cached: Boolean = true): User? {
val cachedUser = cacheDb.userDoa().getById(id)
if (isConnected() and
(!cached or (cachedUser == null) or (cachedUser?.isExpired() == true))
) {
val userProfile = getUserProfile(id)
if (userProfile == null) {
Log.i("User.getById", "User $id not found")
return null
}
val user = User(
id,
userProfile.displayName,
userProfile.avatarUrl,
userProfile.other
)
CoroutineScope(Dispatchers.IO).launch {
cacheDb.userDoa().insert(user.toEntity())
}
return user
}
return cachedUser!!.toDomain()
}
}
}
@@ -15,6 +15,7 @@ import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import coil3.compose.AsyncImagePainter import coil3.compose.AsyncImagePainter
@@ -38,40 +39,43 @@ fun MXCImage(
contentScale: ContentScale = ContentScale.Fit, contentScale: ContentScale = ContentScale.Fit,
contentDescription: String = "" contentDescription: String = ""
) { ) {
val loadState = remember { mutableStateOf(ImageLoadState.Loading) } mxcToUrl(mxcUrl)?.let { url ->
val painter = rememberAsyncImagePainter( val loadState = remember { mutableStateOf(ImageLoadState.Loading) }
model = ImageRequest.Builder(appCtx) val painter = rememberAsyncImagePainter(
.data(mxcToUrl(mxcUrl)) model = ImageRequest.Builder(appCtx)
.httpHeaders( .data(url)
NetworkHeaders.Builder() .httpHeaders(
.set("Authorization", "Bearer $token") NetworkHeaders.Builder()
.set("Cache-Control", "max-age=86400") .set("Authorization", "Bearer $token")
.build() .set("Cache-Control", "max-age=86400")
) .build()
.build(), )
onState = { state -> .build(),
loadState.value = when (state) { onState = { state ->
is AsyncImagePainter.State.Loading -> ImageLoadState.Loading loadState.value = when (state) {
is AsyncImagePainter.State.Success -> ImageLoadState.Success is AsyncImagePainter.State.Loading -> ImageLoadState.Loading
else -> ImageLoadState.Error is AsyncImagePainter.State.Success -> ImageLoadState.Success
else -> ImageLoadState.Error
}
} }
}
)
Box(
modifier = modifier.fillMaxSize()
) {
Image(
painter = painter,
contentDescription = contentDescription,
contentScale = contentScale,
modifier = Modifier.fillMaxSize()
) )
when (loadState.value) { Box(
ImageLoadState.Loading -> CircularProgressIndicator() modifier = modifier.fillMaxSize(),
ImageLoadState.Error -> Icon(Icons.Outlined.Warning, "Error") contentAlignment = Alignment.Center
ImageLoadState.Success -> Unit ) {
Image(
painter = painter,
contentDescription = contentDescription,
contentScale = contentScale,
modifier = Modifier.fillMaxSize()
)
when (loadState.value) {
ImageLoadState.Loading -> CircularProgressIndicator()
ImageLoadState.Error -> Icon(Icons.Outlined.Warning, "Error")
ImageLoadState.Success -> Unit
}
} }
} }
} }
@@ -31,7 +31,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -42,7 +41,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.safety.Safelist import org.jsoup.safety.Safelist
@@ -55,17 +53,14 @@ import ru.risdeveau.pixeldragon.ui.item.MXCImage
@Composable @Composable
fun Room(modifier: Modifier = Modifier, rid: String) { fun Room(modifier: Modifier = Modifier, rid: String) {
var eventsId by remember { mutableStateOf(listOf<Event>()) } var eventsId by remember { mutableStateOf(listOf<Event>()) }
val coroutineScope = rememberCoroutineScope()
val listState = rememberLazyListState() val listState = rememberLazyListState()
LaunchedEffect(true) { LaunchedEffect(Unit) {
coroutineScope.launch { withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) { val readMark = getAccountData(ME!!.userId, rid, "m.fully_read")
val readMark = getAccountData(ME!!.userId, rid, "m.fully_read") val eventsAround = getEventsAround(rid, readMark!!.getString("event_id")) //FIXME: Null check
val eventsAround = getEventsAround(rid, readMark!!.getString("event_id")) //FIXME: Null check eventsId = eventsAround.let {
eventsId = eventsAround.let { it.before + listOf(it.base) + it.after
it.before + listOf(it.base) + it.after
}
} }
} }
} }
@@ -91,7 +86,7 @@ fun EventItem(event: Event) {
"m.room.message" -> Column( "m.room.message" -> Column(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.then ( .then(
if (event.sender != ME!!.userId) if (event.sender != ME!!.userId)
Modifier.padding(end = 16.dp) Modifier.padding(end = 16.dp)
else else
@@ -111,8 +106,11 @@ fun EventItem(event: Event) {
when (val msgtype = event.content.optString("msgtype", null)) { when (val msgtype = event.content.optString("msgtype", null)) {
"m.text" -> when (event.content.optString("format")) { "m.text" -> when (event.content.optString("format")) {
"org.matrix.custom.html" -> "org.matrix.custom.html" -> {
if (event.content.getString("body") == event.content.getString("formatted_body"))
Text(event.content.getString("body"))
HtmlRenderer(event.content.getString("formatted_body")) HtmlRenderer(event.content.getString("formatted_body"))
}
else -> Text(event.content.getString("body")) else -> Text(event.content.getString("body"))
} }
@@ -18,7 +18,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -26,24 +25,20 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ru.risdeveau.pixeldragon.api.getRoom import ru.risdeveau.pixeldragon.repo.Room
import ru.risdeveau.pixeldragon.api.getRooms
import ru.risdeveau.pixeldragon.db.Room
import ru.risdeveau.pixeldragon.ui.item.MXCImage import ru.risdeveau.pixeldragon.ui.item.MXCImage
@Composable @Composable
fun RoomList(modifier: Modifier = Modifier, navController: NavController) { fun RoomList(modifier: Modifier = Modifier, navController: NavController) {
var list by remember { mutableStateOf(listOf<String>()) } var list by remember { mutableStateOf(listOf<Room>()) }
val coroutineScope = rememberCoroutineScope()
val listState = rememberLazyListState() val listState = rememberLazyListState()
// if (itemState.scrollToTop) { // if (itemState.scrollToTop) {
@@ -53,17 +48,13 @@ fun RoomList(modifier: Modifier = Modifier, navController: NavController) {
// } // }
// } // }
LaunchedEffect(true) { LaunchedEffect(Unit) {
coroutineScope.launch { list = withContext(Dispatchers.IO) { Room.getJoined() }
withContext(Dispatchers.IO) {
list = getRooms()
}
}
} }
LazyColumn(modifier = modifier, state = listState) { LazyColumn(modifier = modifier, state = listState) {
items(list) { rid -> items(list) { room ->
RoomItem(rid = rid, navController = navController ) RoomItem(room = room, navController = navController )
} }
item { item {
@@ -75,67 +66,53 @@ fun RoomList(modifier: Modifier = Modifier, navController: NavController) {
} }
@Composable @Composable
fun RoomItem(modifier: Modifier = Modifier, rid: String, navController: NavController) { fun RoomItem(modifier: Modifier = Modifier, room: Room, navController: NavController) {
var room by remember { mutableStateOf<Room?>(null) } val avatarUrl = room.avatarUrl ?: (room.direct?.avatarUrl ?: "")
val scope = rememberCoroutineScope() val name = room.name ?: (room.direct?.name ?: "Unnamed")
LaunchedEffect(true) { Row(
scope.launch { modifier
withContext(Dispatchers.IO) { .padding(8.dp)
room = getRoom(rid) .height((52 + 8 * 2).dp)
} .fillMaxWidth()
} .background(
} color =
if (room.type == "m.space")
if (room != null) { MaterialTheme.colorScheme.tertiary
Row(
modifier
.padding(8.dp)
.height((52 + 8 * 2).dp)
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.clip(RoundedCornerShape(12.dp))
.clickable {
if (room!!.type == "m.space")
navController.navigate("space/$rid")
else else
navController.navigate("room/$rid") MaterialTheme.colorScheme.background
}
.padding(8.dp)
) {
MXCImage(
room!!.avatarUrl ?: "",
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)
}
) )
Column { .clip(RoundedCornerShape(12.dp))
Text( .clickable {
room!!.name ?: "Unnamed", if (room.type == "m.space")
maxLines = 1, navController.navigate("space/${room.id}")
color = MaterialTheme.colorScheme.primary, else
fontSize = MaterialTheme.typography.titleLarge.fontSize navController.navigate("room/${room.id}")
)
} }
} .padding(8.dp)
} else { ) {
Row( MXCImage(
modifier avatarUrl,
.padding(8.dp) modifier = Modifier
.height(80.dp) .padding(end = 4.dp)
.fillMaxWidth() .height(52.dp)
.background(MaterialTheme.colorScheme.background) .width(52.dp)
.clip(RoundedCornerShape(16.dp)) .let {
.padding(8.dp) if (room.type == "m.space")
) { it.clip(RoundedCornerShape(12.dp))
LinearProgressIndicator(Modifier.fillMaxWidth()) else
it.clip(CircleShape)
},
ContentScale.Crop
)
Column {
Text(room.type)
Text(
name,
maxLines = 1,
color = MaterialTheme.colorScheme.primary,
fontSize = MaterialTheme.typography.titleLarge.fontSize
)
} }
} }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.7.3" agp = "8.13.0"
coil = "3.1.0" coil = "3.1.0"
jsoup = "1.20.1" jsoup = "1.20.1"
kotlin = "2.0.21" kotlin = "2.0.21"
+2 -3
View File
@@ -1,12 +1,11 @@
# #
# Created by sweetbread on 21.02.2025, 12:00 # Created by sweetbread
# Copyright (c) 2025. All rights reserved. # Copyright (c) 2025. All rights reserved.
# Last modified 21.02.2025, 12:00
# #
#Thu Feb 20 10:45:47 GMT 2025 #Thu Feb 20 10:45:47 GMT 2025
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists